{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Tutorial: Simple wrapper development\n", "\n", "
\n", "\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/simphony/docs/v3.9.0?filepath=docs%2Fsource%2Fjupyter%2Fwrapper_development.ipynb \"Click to run the tutorial yourself!\")\n", " \n", "
\n", "\n", "\n", "In this tutorial we will implement a very simple simulation wrapper.\n", "It can be used to understand which methods need to be implemented, and how.\n", "\n", "The source files can be found [here](https://github.com/simphony/wrapper-development).\n", "\n", "## Background\n", "Wrappers are the way to extend SimPhoNy to support other back-ends. For an in-depth explanation, you can go to the [wrapper development section](../wrapper_development.md) of the documentation. Here we will explain with more detail what has to be implemented.\n", "\n", "## Requirements\n", "In order to run this code, you need to have the simple_ontology available [here](https://github.com/simphony/wrapper-development/blob/master/osp/wrappers/simple_simulation/simple_ontology.ontology.yml).\n", "\n", "Remember that once you have OSP-core installed and the ontology file locally, you can simply run `pico install `" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# You can download and install the ontology by running this cell\n", "!curl -s https://raw.githubusercontent.com/simphony/wrapper-development/master/osp/wrappers/simple_simulation/simple_ontology.ontology.yml -o simple_ontology.ontology.yml\n", "!pico install simple_ontology.ontology.yml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's get hands on\n", "### Syntactic layer\n", "As you know, SimPhoNy consists of 3 layers, with the wrappers being relevant in the last 2 (interoperability and syntactic layers).\n", "The syntactic layer talks directly to the back-end in a way that it can be understood.\n", "\n", "Since this wrapper aims to be as minimalistic as possible (while still being meaningfull),\n", "we have created a dummy syntactic layer that emulates talking to a simulation tool.\n", "\n", "_Note:_ In order to reduce the amount of code, the docstrings hav been erased. You can refer to the [source file](https://github.com/simphony/wrapper-development/blob/master/osp/wrappers/simple_simulation/simulation_engine.py) for the complete information." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# This is the representation of an atom in the \"engine\"\n", "class Atom():\n", "\n", " def __init__(self, position, velocity):\n", " self.position = position\n", " self.velocity = velocity" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The engine only works with atoms, setting and getting their position and velocities" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class SimulationEngine:\n", " def __init__(self):\n", " self.atoms = list()\n", " print(\"Engine instantiated!\")\n", "\n", " def __str__(self):\n", " return \"Some Engine Connection\"\n", "\n", " def run(self, timesteps=1):\n", " print(\"Now the engine is running\")\n", " for atom in self.atoms:\n", " atom.position += atom.velocity * timesteps\n", "\n", " def add_atom(self, position, velocity):\n", " print(\"Add atom %s with position %s and velocity %s\"\n", " % (len(self.atoms), position, velocity))\n", " self.atoms.append(Atom(position, velocity))\n", "\n", " def update_position(self, idx, position):\n", " print(\"Update atom %s. Setting position to %s\"\n", " % (idx, position))\n", " self.atoms[idx].position = position\n", "\n", " def update_velocity(self, idx, velocity):\n", " print(\"Update atom %s. Setting velocity to %s\"\n", " % (idx, velocity))\n", " self.atoms[idx].velocity = velocity\n", "\n", " def get_velocity(self, idx):\n", " return self.atoms[idx].velocity\n", "\n", " def get_position(self, idx):\n", " return self.atoms[idx].position\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Interoperability layer\n", "Since a lot of 3rd-party tools come with a syntactic layer, the bulk of the work when developping a wrapper for SimPhoNy is here.\n", "\n", "We will explain step by step all the code required." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we import the parent Simulation Wrapper Session and the namespace (ontology).\n", "The engine is not necessary since it is in the previous codebock." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from osp.core.session import SimWrapperSession\n", "# from osp.wrappers.simple_simulation import SimulationEngine\n", "from osp.core.namespaces import simple_ontology " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we will go through each of the methods. \n", "\n", "_Note:_ to be able to break the class into multiple blocks, we will use inheritance, to add a method each time.\n", "In truth, all the definitions should go under one same `class` definition." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The first method is the `__init__`. This method is called when a new object is instantiated.\n", "Here we will call the `__init__` method of the parent class and initialise the necessary elements.\n", "\n", "Most simulation engines will have an internal way to keep track of, for example, particles.\n", "To make sure that the entities in the semantic layer are properly synched, we usually use a _mapper_.\n", "This could be anything from a list or dictionary to a more complex and sofisticated data structure." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimWrapperSession):\n", "\n", " def __init__(self, engine=None, **kwargs):\n", " super().__init__(engine or SimulationEngine(), **kwargs)\n", " self.mapper = dict() # maps uuid to index in the backend\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next comes the output to the `str()` method.\n", "It will be a string returned in `__str__(self)`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", "\n", " def __str__(self):\n", " return \"Simple sample Wrapper Session\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When the `run()` or `commit()` method is called on the session, all the objects that have been added since the last run have to be sent to the back end.\n", "This is done through `_apply_added`.\n", "The method should iterate through all the entities in the buffer and trigger different actions depending on which type of entity it is.\n", "\n", "Remember that we can check the type using the `is_a` method, or querying for the `oclass` attribute of an entity.\n", "\n", "In this example, we will only contact the back end if an atom has been added.\n", "However, normal wrappers will have a lot more comparisons (`if` and `elif`) to determine which entity it is and act accordingly" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", "\n", " # OVERRIDE\n", " def _apply_added(self, root_obj, buffer):\n", " \"\"\"Adds the added cuds to the engine.\"\"\"\n", " for obj in buffer.values():\n", " if obj.is_a(simple_ontology.Atom):\n", " # Add the atom to the mapper\n", " self.mapper[obj.uid] = len(self.mapper)\n", " pos = obj.get(oclass=simple_ontology.Position)[0].value\n", " vel = obj.get(oclass=simple_ontology.Velocity)[0].value\n", " self._engine.add_atom(pos, vel)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just like `_apply_added` is used to modify the engine with the new objects, `_apply_updated` changes the existing ones." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", "\n", " # OVERRIDE\n", " def _apply_updated(self, root_obj, buffer):\n", " \"\"\"Updates the updated cuds in the engine.\"\"\"\n", " for obj in buffer.values():\n", "\n", " # case 1: we change the velocity\n", " if obj.is_a(simple_ontology.Velocity):\n", " atom = obj.get(rel=simple_ontology.isPartOf)[0]\n", " idx = self.mapper[atom.uid]\n", " self._engine.update_velocity(idx, obj.value)\n", "\n", " # case 2: we change the position\n", " elif obj.is_a(simple_ontology.Position):\n", " atom = obj.get(rel=simple_ontology.isPartOf)[0]\n", " idx = self.mapper[atom.uid]\n", " self._engine.update_position(idx, obj.value)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly to the previous methods, `_apply_deleted` should remove entities from the engine.\n", "In this specific case we left it empty to simplify the code (both in the session and the engine classes)." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", " # OVERRIDE\n", " def _apply_deleted(self, root_obj, buffer):\n", " \"\"\"Deletes the deleted cuds from the engine.\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The previous methods synchronise the engine with the cuds, i.e. the communication is from the semantic layer towards the syntactic.\n", "The way to update the cuds with the latest information from the engine is `_load_from_backend`.\n", "\n", "It is most often called when the user calls the `get` on a cuds object that has potentially been changed by the engine.\n", "\n", "When `_load_from_backend` is called for a given cuds object (through its uid), the method should:\n", " - Check if any of the attributes of the object has changed (like the _value_ for a _position_).\n", " - Check if any new children cuds objects have been created (like a static _atom_ that gets a new _velocity_ when another bumps into it).\n", "\n", "However, it does not have to be recursive and check for more than itself. This is because if the user queries any of the contained elements, that will trigger another call to `_load_from_backend`.\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", "\n", " # OVERRIDE\n", " def _load_from_backend(self, uids, expired=None):\n", " \"\"\"Loads the cuds object from the simulation engine\"\"\"\n", " for uid in uids:\n", " if uid in self._registry:\n", " obj = self._registry.get(uid)\n", "\n", " # check whether user wants to load a position\n", " if obj.is_a(simple_ontology.Position):\n", " atom = obj.get(rel=simple_ontology.isPartOf)[0]\n", " idx = self.mapper[atom.uid]\n", " pos = self._engine.get_position(idx)\n", " obj.value = pos\n", "\n", " # check whether user wants to load a velocity\n", " elif obj.is_a(simple_ontology.Velocity):\n", " atom = obj.get(rel=simple_ontology.isPartOf)[0]\n", " idx = self.mapper[atom.uid]\n", " vel = self._engine.get_velocity(idx)\n", " obj.value = vel\n", "\n", " yield obj" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The last method that needs to be overridden is `_run`. It simply has to call the `run` method of the engine.\n", "This could also need to send some information, like the number of steps.\n", "For that reason, the `root_cuds_object` is available for query." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class SimpleSimulationSession(SimpleSimulationSession):\n", "\n", " # OVERRIDE\n", " def _run(self, root_cuds_object):\n", " \"\"\"Call the run command of the engine.\"\"\"\n", " self._engine.run()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can run an example:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "tags": [] }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": "Engine instantiated!\nAdd atom 0 with position [0. 0. 0.] and velocity [0.63000616 0.38951439 0.12717548]\nAdd atom 1 with position [1. 1. 1.] and velocity [0.80816851 0.04562681 0.44983098]\nAdd atom 2 with position [2. 2. 2.] and velocity [0.3849223 0.50767213 0.82963311]\nNow the engine is running\n- Cuds object:\n uuid: a0a97dbe-584d-4764-b085-7b597e323d20\n type: simple_ontology.Material\n superclasses: cuba.Entity, simple_ontology.Material\n description: \n To Be Determined\n\n |_Relationship simple_ontology.hasPart:\n - simple_ontology.Atom cuds object:\n . uuid: 221a1793-c54a-4e42-bdeb-08921617fbac\n . |_Relationship simple_ontology.hasPart:\n . - simple_ontology.Position cuds object:\n . . uuid: db17082e-d9d3-4a48-bce1-9402d4315200\n . . unit: m\n . . value: [0.63000616 0.38951439 0.12717548]\n . - simple_ontology.Velocity cuds object:\n . uuid: fc7d778d-b18a-4b60-a6ab-ba855a2c2874\n . value: [0.63000616 0.38951439 0.12717548]\n . unit: m/s\n - simple_ontology.Atom cuds object:\n . uuid: 222df5d0-c0fe-435b-b5e2-0d5f7ebd32a9\n . |_Relationship simple_ontology.hasPart:\n . - simple_ontology.Position cuds object:\n . . uuid: f0eb08c4-88a3-40de-893f-89473cd194e8\n . . value: [1.80816851 1.04562681 1.44983098]\n . . unit: m\n . - simple_ontology.Velocity cuds object:\n . uuid: d3e7b5ce-3409-4a4e-bbdb-13a2addaee1c\n . value: [0.80816851 0.04562681 0.44983098]\n . unit: m/s\n - simple_ontology.Atom cuds object:\n uuid: 13bfe4ee-32e8-4fbe-bad5-f98f46aa297a\n |_Relationship simple_ontology.hasPart:\n - simple_ontology.Position cuds object:\n . uuid: da5c35f0-afe5-4b56-b5fa-631b72ee32ad\n . value: [2.3849223 2.50767213 2.82963311]\n . unit: m\n - simple_ontology.Velocity cuds object:\n uuid: 9b6c2c1c-3e63-4144-b7c9-d6223c0b79f7\n value: [0.3849223 0.50767213 0.82963311]\n unit: m/s\nUpdate atom 0. Setting velocity to [0. 0. 0.]\nUpdate atom 1. Setting velocity to [0. 0. 0.]\nUpdate atom 2. Setting velocity to [0. 0. 0.]\nNow the engine is running\n- Cuds object:\n uuid: a0a97dbe-584d-4764-b085-7b597e323d20\n type: simple_ontology.Material\n superclasses: cuba.Entity, simple_ontology.Material\n description: \n To Be Determined\n\n |_Relationship simple_ontology.hasPart:\n - simple_ontology.Atom cuds object:\n . uuid: 221a1793-c54a-4e42-bdeb-08921617fbac\n . |_Relationship simple_ontology.hasPart:\n . - simple_ontology.Position cuds object:\n . . uuid: db17082e-d9d3-4a48-bce1-9402d4315200\n . . unit: m\n . . value: [0.63000616 0.38951439 0.12717548]\n . - simple_ontology.Velocity cuds object:\n . uuid: fc7d778d-b18a-4b60-a6ab-ba855a2c2874\n . value: [0. 0. 0.]\n . unit: m/s\n - simple_ontology.Atom cuds object:\n . uuid: 222df5d0-c0fe-435b-b5e2-0d5f7ebd32a9\n . |_Relationship simple_ontology.hasPart:\n . - simple_ontology.Position cuds object:\n . . uuid: f0eb08c4-88a3-40de-893f-89473cd194e8\n . . value: [1.80816851 1.04562681 1.44983098]\n . . unit: m\n . - simple_ontology.Velocity cuds object:\n . uuid: d3e7b5ce-3409-4a4e-bbdb-13a2addaee1c\n . unit: m/s\n . value: [0. 0. 0.]\n - simple_ontology.Atom cuds object:\n uuid: 13bfe4ee-32e8-4fbe-bad5-f98f46aa297a\n |_Relationship simple_ontology.hasPart:\n - simple_ontology.Position cuds object:\n . uuid: da5c35f0-afe5-4b56-b5fa-631b72ee32ad\n . value: [2.3849223 2.50767213 2.82963311]\n . unit: m\n - simple_ontology.Velocity cuds object:\n uuid: 9b6c2c1c-3e63-4144-b7c9-d6223c0b79f7\n value: [0. 0. 0.]\n unit: m/s\n" } ], "source": [ "from osp.core.utils import pretty_print\n", "import numpy as np\n", "\n", "m = simple_ontology.Material()\n", "for i in range(3):\n", " a = m.add(simple_ontology.Atom())\n", " a.add(\n", " simple_ontology.Position(value=[i, i, i], unit=\"m\"),\n", " simple_ontology.Velocity(value=np.random.random(3), unit=\"m/s\")\n", " )\n", "\n", "# Run a simulation\n", "with SimpleSimulationSession() as session:\n", " w = simple_ontology.Wrapper(session=session)\n", " m = w.add(m)\n", " w.session.run()\n", "\n", " pretty_print(m)\n", "\n", " for atom in m.get(rel=simple_ontology.hasPart):\n", " atom.get(oclass=simple_ontology.Velocity)[0].value = [0, 0, 0]\n", " w.session.run()\n", "\n", " pretty_print(m)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] } ], "metadata": {}, "nbformat": 4, "nbformat_minor": 2 }