diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6650266 Binary files /dev/null and b/.DS_Store differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cf2345 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 California Institute of Technology + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 517e2ec..c6f9fc6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # pymerlin + A python modeling framework for Aerie. + ### TODO: @@ -49,7 +51,7 @@ hybrid system, which may be more difficult to characterize. ## Building pymerlin.jar ```shell -cd java +cd pymerlin-java ./gradlew assemble -mv pymerlin/build/libs/pymerlin.jar .. +mv pymerlin/build/libs/pymerlin.jar ../pymerlin/_internal/jars ``` \ No newline at end of file diff --git a/demo/model.py b/demo/model.py index 074b681..f9378d4 100644 --- a/demo/model.py +++ b/demo/model.py @@ -1,4 +1,4 @@ -from pymerlin.decorators import MissionModel +from pymerlin import MissionModel from pymerlin.model_actions import delay, wait_until, spawn diff --git a/demo/simulation_example.ipynb b/demo/simulation_example.ipynb index 082cc7a..44fd54a 100644 --- a/demo/simulation_example.ipynb +++ b/demo/simulation_example.ipynb @@ -2,22 +2,21 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import sys\n", - "sys.path.insert(0, \"/Users/dailis/projects/aerie/python-mission-models/\")" + "sys.path.insert(0, \"/Users/dailis/projects/aerie/pymerlin/\")" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "from pymerlin.decorators import MissionModel\n", - "from pymerlin.framework import Condition\n", + "from pymerlin import MissionModel\n", "from pymerlin.model_actions import delay, wait_until\n", "\n", "@MissionModel\n", @@ -32,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -47,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 12, "metadata": { "collapsed": false, "is_executing": true, @@ -68,13 +67,13 @@ }, "outputs": [], "source": [ - "from pymerlin.framework import ModelType, simulate\n", - "from pymerlin.schedule import Schedule, Directive" + "from pymerlin import simulate\n", + "from pymerlin import Schedule, Directive" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -91,12 +90,12 @@ " (\"00:10:00\", Directive(\"activity1\")),\n", " (\"01:00:00\", Directive(\"activity1\")))\n", "duration = \"01:20:00\"\n", - "profiles, spans, events = simulate(ModelType(Mission), schedule, duration)" + "profiles, spans, events = simulate(Mission, schedule, duration)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "metadata": { "scrolled": true }, @@ -114,7 +113,7 @@ " \n", "
\n", " \n", - " Loading BokehJS ...\n", + " Loading BokehJS ...\n", "
\n" ] }, @@ -287,7 +286,7 @@ " \"\"}};\n", "\n", " function display_loaded(error = null) {\n", - " const el = document.getElementById(\"f79388c3-f12f-4039-96fc-9fd161a3ca52\");\n", + " const el = document.getElementById(\"e477334f-4e67-444f-8a27-68340ce2519c\");\n", " if (el != null) {\n", " const html = (() => {\n", " if (typeof root.Bokeh === \"undefined\") {\n", @@ -421,7 +420,7 @@ " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", " root._bokeh_failed_load = true;\n", " } else if (force !== true) {\n", - " const cell = $(document.getElementById(\"f79388c3-f12f-4039-96fc-9fd161a3ca52\")).parents('.cell').data().cell;\n", + " const cell = $(document.getElementById(\"e477334f-4e67-444f-8a27-68340ce2519c\")).parents('.cell').data().cell;\n", " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", " }\n", " }\n", @@ -437,7 +436,7 @@ " }\n", "}(window));" ], - "application/vnd.bokehjs_load.v0+json": "'use strict';\n(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded(error = null) {\n const el = document.getElementById(\"f79388c3-f12f-4039-96fc-9fd161a3ca52\");\n if (el != null) {\n const html = (() => {\n if (typeof root.Bokeh === \"undefined\") {\n if (error == null) {\n return \"BokehJS is loading ...\";\n } else {\n return \"BokehJS failed to load.\";\n }\n } else {\n const prefix = `BokehJS ${root.Bokeh.version}`;\n if (error == null) {\n return `${prefix} successfully loaded.`;\n } else {\n return `${prefix} encountered errors while loading and may not function as expected.`;\n }\n }\n })();\n el.innerHTML = html;\n\n if (error != null) {\n const wrapper = document.createElement(\"div\");\n wrapper.style.overflow = \"auto\";\n wrapper.style.height = \"5em\";\n wrapper.style.resize = \"vertical\";\n const content = document.createElement(\"div\");\n content.style.fontFamily = \"monospace\";\n content.style.whiteSpace = \"pre-wrap\";\n content.style.backgroundColor = \"rgb(255, 221, 221)\";\n content.textContent = error.stack ?? error.toString();\n wrapper.append(content);\n el.append(wrapper);\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(() => display_loaded(error), 100);\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.4.2.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n try {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n\n } catch (error) {display_loaded(error);throw error;\n }if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"f79388c3-f12f-4039-96fc-9fd161a3ca52\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));" + "application/vnd.bokehjs_load.v0+json": "'use strict';\n(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded(error = null) {\n const el = document.getElementById(\"e477334f-4e67-444f-8a27-68340ce2519c\");\n if (el != null) {\n const html = (() => {\n if (typeof root.Bokeh === \"undefined\") {\n if (error == null) {\n return \"BokehJS is loading ...\";\n } else {\n return \"BokehJS failed to load.\";\n }\n } else {\n const prefix = `BokehJS ${root.Bokeh.version}`;\n if (error == null) {\n return `${prefix} successfully loaded.`;\n } else {\n return `${prefix} encountered errors while loading and may not function as expected.`;\n }\n }\n })();\n el.innerHTML = html;\n\n if (error != null) {\n const wrapper = document.createElement(\"div\");\n wrapper.style.overflow = \"auto\";\n wrapper.style.height = \"5em\";\n wrapper.style.resize = \"vertical\";\n const content = document.createElement(\"div\");\n content.style.fontFamily = \"monospace\";\n content.style.whiteSpace = \"pre-wrap\";\n content.style.backgroundColor = \"rgb(255, 221, 221)\";\n content.textContent = error.stack ?? error.toString();\n wrapper.append(content);\n el.append(wrapper);\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(() => display_loaded(error), 100);\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.4.2.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n try {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n\n } catch (error) {display_loaded(error);throw error;\n }if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"e477334f-4e67-444f-8a27-68340ce2519c\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));" }, "metadata": {}, "output_type": "display_data" @@ -446,7 +445,7 @@ "data": { "text/html": [ "\n", - "
\n" + "
\n" ] }, "metadata": {}, @@ -457,8 +456,8 @@ "application/javascript": [ "(function(root) {\n", " function embed_document(root) {\n", - " const docs_json = {\"5855ae5d-34c6-49c0-80a0-9d0bd29f80d7\":{\"version\":\"3.4.2\",\"title\":\"Bokeh Application\",\"roots\":[{\"type\":\"object\",\"name\":\"GridPlot\",\"id\":\"p1105\",\"attributes\":{\"rows\":null,\"cols\":null,\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1104\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1098\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"p1025\"},{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"p1073\"}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1099\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"p1026\",\"attributes\":{\"renderers\":\"auto\"}},{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"p1074\",\"attributes\":{\"renderers\":\"auto\"}}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1100\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"p1027\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"p1028\",\"attributes\":{\"syncable\":false,\"level\":\"overlay\",\"visible\":false,\"left\":{\"type\":\"number\",\"value\":\"nan\"},\"right\":{\"type\":\"number\",\"value\":\"nan\"},\"top\":{\"type\":\"number\",\"value\":\"nan\"},\"bottom\":{\"type\":\"number\",\"value\":\"nan\"},\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5}}}},{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"p1075\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"p1076\",\"attributes\":{\"syncable\":false,\"level\":\"overlay\",\"visible\":false,\"left\":{\"type\":\"number\",\"value\":\"nan\"},\"right\":{\"type\":\"number\",\"value\":\"nan\"},\"top\":{\"type\":\"number\",\"value\":\"nan\"},\"bottom\":{\"type\":\"number\",\"value\":\"nan\"},\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5}}}}]}},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1101\"},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1102\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"p1034\"},{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"p1082\"}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1103\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"HelpTool\",\"id\":\"p1035\"},{\"type\":\"object\",\"name\":\"HelpTool\",\"id\":\"p1083\"}]}}]}},\"children\":[[{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"p1001\",\"attributes\":{\"width\":400,\"height\":120,\"x_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"p1011\",\"attributes\":{\"end\":4800000000}},\"y_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"p1012\",\"attributes\":{\"start\":2,\"end\":-1}},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1013\"},\"y_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1014\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"p1004\",\"attributes\":{\"text\":\"Spans\"}},\"outline_line_color\":null,\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1040\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1036\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1037\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1038\"},\"data\":{\"type\":\"map\",\"entries\":[[\"y\",[0,1]],[\"left\",[600000000,3600000000]],[\"right\",[2400000000,{\"type\":\"number\",\"value\":\"+inf\"}]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1041\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1042\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"HBar\",\"id\":\"p1039\",\"attributes\":{\"y\":{\"type\":\"field\",\"field\":\"y\"},\"height\":{\"type\":\"value\",\"value\":0.9},\"left\":{\"type\":\"field\",\"field\":\"left\"},\"right\":{\"type\":\"field\",\"field\":\"right\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#b3de69\"}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1010\",\"attributes\":{\"tools\":[{\"id\":\"p1025\"},{\"id\":\"p1026\"},{\"id\":\"p1027\"},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1033\"},{\"id\":\"p1034\"},{\"id\":\"p1035\"}]}},\"toolbar_location\":null,\"left\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1020\",\"attributes\":{\"visible\":false,\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1021\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1022\"},\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1023\"}}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1015\",\"attributes\":{\"visible\":false,\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1016\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1017\"},\"axis_label\":\"Time (microseconds)\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1018\"}}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1019\",\"attributes\":{\"axis\":{\"id\":\"p1015\"}}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1024\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"p1020\"},\"grid_line_color\":null}},{\"type\":\"object\",\"name\":\"LabelSet\",\"id\":\"p1046\",\"attributes\":{\"source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1043\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1044\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1045\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[700000000,3700000000]],[\"y\",[-0.45,0.55]],[\"labels\",[\"activity1\",\"activity1\"]]]}}},\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"text\":{\"type\":\"field\",\"field\":\"labels\"},\"text_color\":{\"type\":\"value\",\"value\":\"#555555\"},\"text_font_size\":{\"type\":\"value\",\"value\":\"11px\"}}}]}},0,0],[{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"p1050\",\"attributes\":{\"width\":400,\"height\":130,\"x_range\":{\"id\":\"p1011\"},\"y_range\":{\"type\":\"object\",\"name\":\"FactorRange\",\"id\":\"p1060\",\"attributes\":{\"factors\":[\"/cell2\",\"/cell1\"]}},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1061\"},\"y_scale\":{\"type\":\"object\",\"name\":\"CategoricalScale\",\"id\":\"p1062\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"p1053\",\"attributes\":{\"text\":\"Resources\"}},\"outline_line_color\":null,\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1088\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1084\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1085\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1086\"},\"data\":{\"type\":\"map\",\"entries\":[[\"y\",[\"/cell1\",\"/cell1\",\"/cell1\",\"/cell1\",\"/cell2\",\"/cell2\",\"/cell2\",\"/cell2\"]],[\"left\",[0,600000000,2400000000,3600000000,0,600000000,2400000000,3600000000]],[\"right\",[600000000,2400000000,3600000000,4800000000,600000000,2400000000,3600000000,4800000000]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1089\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1090\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"HBar\",\"id\":\"p1087\",\"attributes\":{\"y\":{\"type\":\"field\",\"field\":\"y\"},\"height\":{\"type\":\"value\",\"value\":0.9},\"left\":{\"type\":\"field\",\"field\":\"left\"},\"right\":{\"type\":\"field\",\"field\":\"right\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#b3de69\"}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1059\",\"attributes\":{\"tools\":[{\"id\":\"p1073\"},{\"id\":\"p1074\"},{\"id\":\"p1075\"},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1081\"},{\"id\":\"p1082\"},{\"id\":\"p1083\"}]}},\"toolbar_location\":null,\"left\":[{\"type\":\"object\",\"name\":\"CategoricalAxis\",\"id\":\"p1068\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"CategoricalTicker\",\"id\":\"p1069\"},\"formatter\":{\"type\":\"object\",\"name\":\"CategoricalTickFormatter\",\"id\":\"p1070\"},\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1071\"}}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1063\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1064\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1065\"},\"axis_label\":\"Time (microseconds)\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1066\"}}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1067\",\"attributes\":{\"axis\":{\"id\":\"p1063\"}}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1072\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"p1068\"},\"grid_line_color\":null}},{\"type\":\"object\",\"name\":\"LabelSet\",\"id\":\"p1094\",\"attributes\":{\"source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1091\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1092\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1093\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[100000000.0,1300000000.0,2800000000.0,4000000000.0,100000000.0,1300000000.0,2800000000.0,4000000000.0]],[\"y\",[\"/cell1\",\"/cell1\",\"/cell1\",\"/cell1\",\"/cell2\",\"/cell2\",\"/cell2\",\"/cell2\"]],[\"labels\",[\"init\",\"foo\",\"bar\",\"foo\",\"init\",\"foo\",\"bar\",\"foo\"]]]}}},\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"text\":{\"type\":\"field\",\"field\":\"labels\"},\"text_color\":{\"type\":\"value\",\"value\":\"#555555\"},\"text_font_size\":{\"type\":\"value\",\"value\":\"11px\"}}}]}},1,0]]}}]}};\n", - " const render_items = [{\"docid\":\"5855ae5d-34c6-49c0-80a0-9d0bd29f80d7\",\"roots\":{\"p1105\":\"a90df73e-8ba1-4a0c-ad9f-6bf5fbbc9960\"},\"root_ids\":[\"p1105\"]}];\n", + " const docs_json = {\"055e0f38-66be-4ca8-bcbb-7cec026e4c31\":{\"version\":\"3.4.2\",\"title\":\"Bokeh Application\",\"roots\":[{\"type\":\"object\",\"name\":\"GridPlot\",\"id\":\"p1105\",\"attributes\":{\"rows\":null,\"cols\":null,\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1104\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1098\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"p1025\"},{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"p1073\"}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1099\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"p1026\",\"attributes\":{\"renderers\":\"auto\"}},{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"p1074\",\"attributes\":{\"renderers\":\"auto\"}}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1100\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"p1027\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"p1028\",\"attributes\":{\"syncable\":false,\"level\":\"overlay\",\"visible\":false,\"left\":{\"type\":\"number\",\"value\":\"nan\"},\"right\":{\"type\":\"number\",\"value\":\"nan\"},\"top\":{\"type\":\"number\",\"value\":\"nan\"},\"bottom\":{\"type\":\"number\",\"value\":\"nan\"},\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5}}}},{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"p1075\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"p1076\",\"attributes\":{\"syncable\":false,\"level\":\"overlay\",\"visible\":false,\"left\":{\"type\":\"number\",\"value\":\"nan\"},\"right\":{\"type\":\"number\",\"value\":\"nan\"},\"top\":{\"type\":\"number\",\"value\":\"nan\"},\"bottom\":{\"type\":\"number\",\"value\":\"nan\"},\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5}}}}]}},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1101\"},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1102\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"p1034\"},{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"p1082\"}]}},{\"type\":\"object\",\"name\":\"ToolProxy\",\"id\":\"p1103\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"HelpTool\",\"id\":\"p1035\"},{\"type\":\"object\",\"name\":\"HelpTool\",\"id\":\"p1083\"}]}}]}},\"children\":[[{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"p1001\",\"attributes\":{\"width\":400,\"height\":120,\"x_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"p1011\",\"attributes\":{\"end\":4800000000}},\"y_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"p1012\",\"attributes\":{\"start\":2,\"end\":-1}},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1013\"},\"y_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1014\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"p1004\",\"attributes\":{\"text\":\"Spans\"}},\"outline_line_color\":null,\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1040\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1036\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1037\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1038\"},\"data\":{\"type\":\"map\",\"entries\":[[\"y\",[0,1]],[\"left\",[600000000,3600000000]],[\"right\",[2400000000,{\"type\":\"number\",\"value\":\"+inf\"}]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1041\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1042\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"HBar\",\"id\":\"p1039\",\"attributes\":{\"y\":{\"type\":\"field\",\"field\":\"y\"},\"height\":{\"type\":\"value\",\"value\":0.9},\"left\":{\"type\":\"field\",\"field\":\"left\"},\"right\":{\"type\":\"field\",\"field\":\"right\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#b3de69\"}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1010\",\"attributes\":{\"tools\":[{\"id\":\"p1025\"},{\"id\":\"p1026\"},{\"id\":\"p1027\"},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1033\"},{\"id\":\"p1034\"},{\"id\":\"p1035\"}]}},\"toolbar_location\":null,\"left\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1020\",\"attributes\":{\"visible\":false,\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1021\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1022\"},\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1023\"}}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1015\",\"attributes\":{\"visible\":false,\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1016\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1017\"},\"axis_label\":\"Time (microseconds)\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1018\"}}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1019\",\"attributes\":{\"axis\":{\"id\":\"p1015\"}}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1024\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"p1020\"},\"grid_line_color\":null}},{\"type\":\"object\",\"name\":\"LabelSet\",\"id\":\"p1046\",\"attributes\":{\"source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1043\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1044\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1045\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[700000000,3700000000]],[\"y\",[-0.45,0.55]],[\"labels\",[\"activity1\",\"activity1\"]]]}}},\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"text\":{\"type\":\"field\",\"field\":\"labels\"},\"text_color\":{\"type\":\"value\",\"value\":\"#555555\"},\"text_font_size\":{\"type\":\"value\",\"value\":\"11px\"}}}]}},0,0],[{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"p1050\",\"attributes\":{\"width\":400,\"height\":130,\"x_range\":{\"id\":\"p1011\"},\"y_range\":{\"type\":\"object\",\"name\":\"FactorRange\",\"id\":\"p1060\",\"attributes\":{\"factors\":[\"/cell2\",\"/cell1\"]}},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"p1061\"},\"y_scale\":{\"type\":\"object\",\"name\":\"CategoricalScale\",\"id\":\"p1062\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"p1053\",\"attributes\":{\"text\":\"Resources\"}},\"outline_line_color\":null,\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"p1088\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1084\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1085\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1086\"},\"data\":{\"type\":\"map\",\"entries\":[[\"y\",[\"/cell1\",\"/cell1\",\"/cell1\",\"/cell1\",\"/cell2\",\"/cell2\",\"/cell2\",\"/cell2\"]],[\"left\",[0,600000000,2400000000,3600000000,0,600000000,2400000000,3600000000]],[\"right\",[600000000,2400000000,3600000000,4800000000,600000000,2400000000,3600000000,4800000000]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"p1089\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"p1090\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"HBar\",\"id\":\"p1087\",\"attributes\":{\"y\":{\"type\":\"field\",\"field\":\"y\"},\"height\":{\"type\":\"value\",\"value\":0.9},\"left\":{\"type\":\"field\",\"field\":\"left\"},\"right\":{\"type\":\"field\",\"field\":\"right\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#b3de69\"}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"p1059\",\"attributes\":{\"tools\":[{\"id\":\"p1073\"},{\"id\":\"p1074\"},{\"id\":\"p1075\"},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"p1081\"},{\"id\":\"p1082\"},{\"id\":\"p1083\"}]}},\"toolbar_location\":null,\"left\":[{\"type\":\"object\",\"name\":\"CategoricalAxis\",\"id\":\"p1068\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"CategoricalTicker\",\"id\":\"p1069\"},\"formatter\":{\"type\":\"object\",\"name\":\"CategoricalTickFormatter\",\"id\":\"p1070\"},\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1071\"}}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"p1063\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"p1064\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"p1065\"},\"axis_label\":\"Time (microseconds)\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"p1066\"}}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1067\",\"attributes\":{\"axis\":{\"id\":\"p1063\"}}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"p1072\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"p1068\"},\"grid_line_color\":null}},{\"type\":\"object\",\"name\":\"LabelSet\",\"id\":\"p1094\",\"attributes\":{\"source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"p1091\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"p1092\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"p1093\"},\"data\":{\"type\":\"map\",\"entries\":[[\"x\",[100000000.0,1300000000.0,2800000000.0,4000000000.0,100000000.0,1300000000.0,2800000000.0,4000000000.0]],[\"y\",[\"/cell1\",\"/cell1\",\"/cell1\",\"/cell1\",\"/cell2\",\"/cell2\",\"/cell2\",\"/cell2\"]],[\"labels\",[\"init\",\"foo\",\"bar\",\"foo\",\"init\",\"foo\",\"bar\",\"foo\"]]]}}},\"x\":{\"type\":\"field\",\"field\":\"x\"},\"y\":{\"type\":\"field\",\"field\":\"y\"},\"text\":{\"type\":\"field\",\"field\":\"labels\"},\"text_color\":{\"type\":\"value\",\"value\":\"#555555\"},\"text_font_size\":{\"type\":\"value\",\"value\":\"11px\"}}}]}},1,0]]}}]}};\n", + " const render_items = [{\"docid\":\"055e0f38-66be-4ca8-bcbb-7cec026e4c31\",\"roots\":{\"p1105\":\"a25154b2-3568-47d8-bb57-a3b33dceb6c4\"},\"root_ids\":[\"p1105\"]}];\n", " void root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n", " }\n", " if (root.Bokeh !== undefined) {\n", @@ -491,7 +490,7 @@ } ], "source": [ - "from pymerlin.plot import plot_spans, plot_profiles\n", + "from pymerlin._internal._plot import plot_spans, plot_profiles\n", "from bokeh.plotting import show\n", "\n", "p1 = plot_spans(spans, duration)\n", @@ -503,6 +502,13 @@ "\n", "show(gridplot([[p1], [p2]]))" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/0_quickstart.md b/docs/0_quickstart.md new file mode 100644 index 0000000..0d96777 --- /dev/null +++ b/docs/0_quickstart.md @@ -0,0 +1,20 @@ +# Quickstart + +This section describes how to get started with `pymerlin` 🎉 + +## Installation + +First, make sure you have Java 21 JRE and python 12 installed on your machine. + +- Java: https://adoptium.net/temurin/releases/ +- Python: https://www.python.org/downloads/release/python-3120/ + +Once those are ready, make a [python virtual environment](https://docs.python.org/3/library/venv.html) for your project + +After activating that environment, install `pymerlin` with the following command: + +```shell +pip install pymerlin +``` + + diff --git a/docs/1_tutorials/1_gettingstarted.md b/docs/1_tutorials/1_gettingstarted.md new file mode 100644 index 0000000..9f9b999 --- /dev/null +++ b/docs/1_tutorials/1_gettingstarted.md @@ -0,0 +1,3 @@ +# Getting Started + +This is a tutorial \ No newline at end of file diff --git a/docs/1_tutorials/index.md b/docs/1_tutorials/index.md new file mode 100644 index 0000000..4634db0 --- /dev/null +++ b/docs/1_tutorials/index.md @@ -0,0 +1,8 @@ +# Tutorials + +```{toctree} +:hidden: + +1_gettingstarted +tutorial2 +``` diff --git a/docs/1_tutorials/tutorial2.md b/docs/1_tutorials/tutorial2.md new file mode 100644 index 0000000..069381d --- /dev/null +++ b/docs/1_tutorials/tutorial2.md @@ -0,0 +1,3 @@ +# Going further + +This is a tutorial \ No newline at end of file diff --git a/docs/2_guides/datamodel.md b/docs/2_guides/datamodel.md new file mode 100644 index 0000000..8c7e147 --- /dev/null +++ b/docs/2_guides/datamodel.md @@ -0,0 +1,3 @@ +# Modeling Data Volume + +This is a tutorial \ No newline at end of file diff --git a/docs/2_guides/index.md b/docs/2_guides/index.md new file mode 100644 index 0000000..96b0206 --- /dev/null +++ b/docs/2_guides/index.md @@ -0,0 +1,10 @@ +# Guides + +```{toctree} +:hidden: + +jupyter +datamodel +powermodel +telecom +``` diff --git a/docs/2_guides/jupyter.md b/docs/2_guides/jupyter.md new file mode 100644 index 0000000..81c5cad --- /dev/null +++ b/docs/2_guides/jupyter.md @@ -0,0 +1,3 @@ +# How to use pymerlin in Jupyter + +This is a tutorial \ No newline at end of file diff --git a/docs/2_guides/powermodel.md b/docs/2_guides/powermodel.md new file mode 100644 index 0000000..794dc3d --- /dev/null +++ b/docs/2_guides/powermodel.md @@ -0,0 +1,3 @@ +# Modeling Power + +This is a tutorial \ No newline at end of file diff --git a/docs/2_guides/telecom.md b/docs/2_guides/telecom.md new file mode 100644 index 0000000..4784bde --- /dev/null +++ b/docs/2_guides/telecom.md @@ -0,0 +1,3 @@ +# Modeling Telecom + +This is a tutorial \ No newline at end of file diff --git a/docs/3_explanation/fidelity.md b/docs/3_explanation/fidelity.md new file mode 100644 index 0000000..a2a7ed7 --- /dev/null +++ b/docs/3_explanation/fidelity.md @@ -0,0 +1,4 @@ +# Tuning Fidelity + +pymerlin (and Aerie's) strength is its ability to compose several subsystem models and observe behavior of the +integrated system. It is not necessarily the best tool for detailed modeling of individual subsystems. \ No newline at end of file diff --git a/docs/3_explanation/index.md b/docs/3_explanation/index.md new file mode 100644 index 0000000..ca4693f --- /dev/null +++ b/docs/3_explanation/index.md @@ -0,0 +1,8 @@ +# Background +Here, you can learn more about some of the theory of simulation, planning, and simulation. + +```{toctree} +simulation +planning +fidelity +``` diff --git a/docs/3_explanation/planning.md b/docs/3_explanation/planning.md new file mode 100644 index 0000000..5035206 --- /dev/null +++ b/docs/3_explanation/planning.md @@ -0,0 +1,3 @@ +# Planning and Scheduling + +This is a tutorial \ No newline at end of file diff --git a/docs/3_explanation/simulation.md b/docs/3_explanation/simulation.md new file mode 100644 index 0000000..65e2860 --- /dev/null +++ b/docs/3_explanation/simulation.md @@ -0,0 +1,3 @@ +# Discrete Event Simulation + +This is a tutorial \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst new file mode 100644 index 0000000..6c9ea84 --- /dev/null +++ b/docs/_templates/custom-class-template.rst @@ -0,0 +1,32 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: <-- add at least this line + :show-inheritance: <-- plus I want to show inheritance... + :inherited-members: <-- ...and inherited members too + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst new file mode 100644 index 0000000..0b9c4e2 --- /dev/null +++ b/docs/_templates/custom-module-template.rst @@ -0,0 +1,66 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module Attributes + + .. autosummary:: + :toctree: <-- add this line + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: <-- add this line + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: <-- add this line + :template: custom-class-template.rst <-- add this line + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: <-- add this line + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :template: custom-module-template.rst <-- add this line + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst new file mode 100644 index 0000000..bce112e --- /dev/null +++ b/docs/apidocs/index.rst @@ -0,0 +1,10 @@ + +API Reference +============= + +Here, you can find the details of functions, classes, and data structures provided by the `pymerlin` package. + +.. toctree:: + :titlesonly: + + pymerlin/pymerlin \ No newline at end of file diff --git a/docs/apidocs/pymerlin/pymerlin.duration.rst b/docs/apidocs/pymerlin/pymerlin.duration.rst new file mode 100644 index 0000000..a3cb82f --- /dev/null +++ b/docs/apidocs/pymerlin/pymerlin.duration.rst @@ -0,0 +1,216 @@ +:py:mod:`pymerlin.duration` +=========================== + +.. py:module:: pymerlin.duration + +.. autodoc2-docstring:: pymerlin.duration + :parser: myst + :allowtitles: + +Module Contents +--------------- + +Classes +~~~~~~~ + +.. list-table:: + :class: autosummary longtable + :align: left + + * - :py:obj:`Duration ` + - .. autodoc2-docstring:: pymerlin.duration.Duration + :parser: myst + :summary: + +Data +~~~~ + +.. list-table:: + :class: autosummary longtable + :align: left + + * - :py:obj:`ZERO ` + - .. autodoc2-docstring:: pymerlin.duration.ZERO + :parser: myst + :summary: + * - :py:obj:`MICROSECOND ` + - .. autodoc2-docstring:: pymerlin.duration.MICROSECOND + :parser: myst + :summary: + * - :py:obj:`MICROSECONDS ` + - .. autodoc2-docstring:: pymerlin.duration.MICROSECONDS + :parser: myst + :summary: + * - :py:obj:`MILLISECOND ` + - .. autodoc2-docstring:: pymerlin.duration.MILLISECOND + :parser: myst + :summary: + * - :py:obj:`MILLISECONDS ` + - .. autodoc2-docstring:: pymerlin.duration.MILLISECONDS + :parser: myst + :summary: + * - :py:obj:`SECOND ` + - .. autodoc2-docstring:: pymerlin.duration.SECOND + :parser: myst + :summary: + * - :py:obj:`SECONDS ` + - .. autodoc2-docstring:: pymerlin.duration.SECONDS + :parser: myst + :summary: + * - :py:obj:`MINUTE ` + - .. autodoc2-docstring:: pymerlin.duration.MINUTE + :parser: myst + :summary: + * - :py:obj:`MINUTES ` + - .. autodoc2-docstring:: pymerlin.duration.MINUTES + :parser: myst + :summary: + * - :py:obj:`HOUR ` + - .. autodoc2-docstring:: pymerlin.duration.HOUR + :parser: myst + :summary: + * - :py:obj:`HOURS ` + - .. autodoc2-docstring:: pymerlin.duration.HOURS + :parser: myst + :summary: + +API +~~~ + +.. py:class:: Duration(micros) + :canonical: pymerlin.duration.Duration + + .. autodoc2-docstring:: pymerlin.duration.Duration + :parser: myst + + .. rubric:: Initialization + + .. autodoc2-docstring:: pymerlin.duration.Duration.__init__ + :parser: myst + + .. py:method:: times(scalar) + :canonical: pymerlin.duration.Duration.times + + .. autodoc2-docstring:: pymerlin.duration.Duration.times + :parser: myst + + .. py:method:: plus(other) + :canonical: pymerlin.duration.Duration.plus + + .. autodoc2-docstring:: pymerlin.duration.Duration.plus + :parser: myst + + .. py:method:: negate() + :canonical: pymerlin.duration.Duration.negate + + .. autodoc2-docstring:: pymerlin.duration.Duration.negate + :parser: myst + + .. py:method:: of(scalar, unit) + :canonical: pymerlin.duration.Duration.of + :staticmethod: + + .. autodoc2-docstring:: pymerlin.duration.Duration.of + :parser: myst + + .. py:method:: from_string(duration_string) + :canonical: pymerlin.duration.Duration.from_string + :staticmethod: + + .. autodoc2-docstring:: pymerlin.duration.Duration.from_string + :parser: myst + + .. py:method:: __repr__() + :canonical: pymerlin.duration.Duration.__repr__ + + .. py:method:: __eq__(other) + :canonical: pymerlin.duration.Duration.__eq__ + + .. py:method:: __gt__(other) + :canonical: pymerlin.duration.Duration.__gt__ + + .. py:method:: __ge__(other) + :canonical: pymerlin.duration.Duration.__ge__ + + .. py:method:: __lt__(other) + :canonical: pymerlin.duration.Duration.__lt__ + + .. py:method:: __le__(other) + :canonical: pymerlin.duration.Duration.__le__ + +.. py:data:: ZERO + :canonical: pymerlin.duration.ZERO + :value: None + + .. autodoc2-docstring:: pymerlin.duration.ZERO + :parser: myst + +.. py:data:: MICROSECOND + :canonical: pymerlin.duration.MICROSECOND + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MICROSECOND + :parser: myst + +.. py:data:: MICROSECONDS + :canonical: pymerlin.duration.MICROSECONDS + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MICROSECONDS + :parser: myst + +.. py:data:: MILLISECOND + :canonical: pymerlin.duration.MILLISECOND + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MILLISECOND + :parser: myst + +.. py:data:: MILLISECONDS + :canonical: pymerlin.duration.MILLISECONDS + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MILLISECONDS + :parser: myst + +.. py:data:: SECOND + :canonical: pymerlin.duration.SECOND + :value: None + + .. autodoc2-docstring:: pymerlin.duration.SECOND + :parser: myst + +.. py:data:: SECONDS + :canonical: pymerlin.duration.SECONDS + :value: None + + .. autodoc2-docstring:: pymerlin.duration.SECONDS + :parser: myst + +.. py:data:: MINUTE + :canonical: pymerlin.duration.MINUTE + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MINUTE + :parser: myst + +.. py:data:: MINUTES + :canonical: pymerlin.duration.MINUTES + :value: None + + .. autodoc2-docstring:: pymerlin.duration.MINUTES + :parser: myst + +.. py:data:: HOUR + :canonical: pymerlin.duration.HOUR + :value: None + + .. autodoc2-docstring:: pymerlin.duration.HOUR + :parser: myst + +.. py:data:: HOURS + :canonical: pymerlin.duration.HOURS + :value: None + + .. autodoc2-docstring:: pymerlin.duration.HOURS + :parser: myst diff --git a/docs/apidocs/pymerlin/pymerlin.model_actions.rst b/docs/apidocs/pymerlin/pymerlin.model_actions.rst new file mode 100644 index 0000000..112c81f --- /dev/null +++ b/docs/apidocs/pymerlin/pymerlin.model_actions.rst @@ -0,0 +1,178 @@ +:py:mod:`pymerlin.model_actions` +================================ + +.. py:module:: pymerlin.model_actions + +.. autodoc2-docstring:: pymerlin.model_actions + :parser: myst + :allowtitles: + +Module Contents +--------------- + +Functions +~~~~~~~~~ + +.. list-table:: + :class: autosummary longtable + :align: left + + * - :py:obj:`_context ` + - .. autodoc2-docstring:: pymerlin.model_actions._context + :parser: myst + :summary: + * - :py:obj:`_set_context ` + - .. autodoc2-docstring:: pymerlin.model_actions._set_context + :parser: myst + :summary: + * - :py:obj:`_clear_context ` + - .. autodoc2-docstring:: pymerlin.model_actions._clear_context + :parser: myst + :summary: + * - :py:obj:`_set_yield_callback ` + - .. autodoc2-docstring:: pymerlin.model_actions._set_yield_callback + :parser: myst + :summary: + * - :py:obj:`_clear_yield_callback ` + - .. autodoc2-docstring:: pymerlin.model_actions._clear_yield_callback + :parser: myst + :summary: + * - :py:obj:`delay ` + - .. autodoc2-docstring:: pymerlin.model_actions.delay + :parser: myst + :summary: + * - :py:obj:`spawn ` + - .. autodoc2-docstring:: pymerlin.model_actions.spawn + :parser: myst + :summary: + * - :py:obj:`call ` + - .. autodoc2-docstring:: pymerlin.model_actions.call + :parser: myst + :summary: + * - :py:obj:`wait_until ` + - .. autodoc2-docstring:: pymerlin.model_actions.wait_until + :parser: myst + :summary: + * - :py:obj:`_yield_with ` + - .. autodoc2-docstring:: pymerlin.model_actions._yield_with + :parser: myst + :summary: + +Data +~~~~ + +.. list-table:: + :class: autosummary longtable + :align: left + + * - :py:obj:`Completed ` + - .. autodoc2-docstring:: pymerlin.model_actions.Completed + :parser: myst + :summary: + * - :py:obj:`Delayed ` + - .. autodoc2-docstring:: pymerlin.model_actions.Delayed + :parser: myst + :summary: + * - :py:obj:`Calling ` + - .. autodoc2-docstring:: pymerlin.model_actions.Calling + :parser: myst + :summary: + * - :py:obj:`Awaiting ` + - .. autodoc2-docstring:: pymerlin.model_actions.Awaiting + :parser: myst + :summary: + +API +~~~ + +.. py:data:: Completed + :canonical: pymerlin.model_actions.Completed + :value: 'namedtuple(...)' + + .. autodoc2-docstring:: pymerlin.model_actions.Completed + :parser: myst + +.. py:data:: Delayed + :canonical: pymerlin.model_actions.Delayed + :value: 'namedtuple(...)' + + .. autodoc2-docstring:: pymerlin.model_actions.Delayed + :parser: myst + +.. py:data:: Calling + :canonical: pymerlin.model_actions.Calling + :value: 'namedtuple(...)' + + .. autodoc2-docstring:: pymerlin.model_actions.Calling + :parser: myst + +.. py:data:: Awaiting + :canonical: pymerlin.model_actions.Awaiting + :value: 'namedtuple(...)' + + .. autodoc2-docstring:: pymerlin.model_actions.Awaiting + :parser: myst + +.. py:function:: _context(scheduler, spawner=None) + :canonical: pymerlin.model_actions._context + + .. autodoc2-docstring:: pymerlin.model_actions._context + :parser: myst + +.. py:function:: _set_context(context, spawner) + :canonical: pymerlin.model_actions._set_context + + .. autodoc2-docstring:: pymerlin.model_actions._set_context + :parser: myst + +.. py:function:: _clear_context() + :canonical: pymerlin.model_actions._clear_context + + .. autodoc2-docstring:: pymerlin.model_actions._clear_context + :parser: myst + +.. py:function:: _set_yield_callback(callback) + :canonical: pymerlin.model_actions._set_yield_callback + + .. autodoc2-docstring:: pymerlin.model_actions._set_yield_callback + :parser: myst + +.. py:function:: _clear_yield_callback() + :canonical: pymerlin.model_actions._clear_yield_callback + + .. autodoc2-docstring:: pymerlin.model_actions._clear_yield_callback + :parser: myst + +.. py:function:: delay(duration) + :canonical: pymerlin.model_actions.delay + :async: + + .. autodoc2-docstring:: pymerlin.model_actions.delay + :parser: myst + +.. py:function:: spawn(child) + :canonical: pymerlin.model_actions.spawn + + .. autodoc2-docstring:: pymerlin.model_actions.spawn + :parser: myst + +.. py:function:: call(child) + :canonical: pymerlin.model_actions.call + :async: + + .. autodoc2-docstring:: pymerlin.model_actions.call + :parser: myst + +.. py:function:: wait_until(condition) + :canonical: pymerlin.model_actions.wait_until + :async: + + .. autodoc2-docstring:: pymerlin.model_actions.wait_until + :parser: myst + +.. py:function:: _yield_with(status) + :canonical: pymerlin.model_actions._yield_with + :async: + + .. autodoc2-docstring:: pymerlin.model_actions._yield_with + :parser: myst diff --git a/docs/apidocs/pymerlin/pymerlin.rst b/docs/apidocs/pymerlin/pymerlin.rst new file mode 100644 index 0000000..057d486 --- /dev/null +++ b/docs/apidocs/pymerlin/pymerlin.rst @@ -0,0 +1,18 @@ +:py:mod:`pymerlin` +================== + +.. py:module:: pymerlin + +.. autodoc2-docstring:: pymerlin + :parser: myst + :allowtitles: + +Submodules +---------- + +.. toctree:: + :titlesonly: + :maxdepth: 1 + + pymerlin.duration + pymerlin.model_actions diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b499867 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,16 @@ +# Architecture + +pymerlin is to merlin as `pyspark `_ is to `spark `_. +This means that pymerlin uses [py4j](https://www.py4j.org/) as a bridge +between a python process and a java process. This allows pymerlin to use the Aerie simulation engine directly, without +having to re-implement it in python. + +This means that running `simulate` starts a subprocess using `java -jar /path/to/pymerlin.jar`. + +## Approachability over performance + +The main tenet of pymerlin is approachability, and its aim is to enable rapid prototyping of models and activities. +While where possible, performance will be considered, it is expected that someone who wants to seriously engineer the +performance of their simulation will port their code to Java - which has the double benefit of removing socket +communication overhead, as well as giving the engineer a single Java process to instrument and analyze, rather than a +hybrid system, which may be more difficult to characterize. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..607530c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,60 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'pymerlin' +copyright = '2024, Matthew Dailis' +author = 'Matthew Dailis' +release = '0.0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +import os +import sys + +sys.path.insert(0, os.path.abspath('..')) # Source code dir relative to this file + +extensions = [ + "autodoc2", + 'myst_parser', + # 'sphinx.ext.autodoc', # Core library for html generation from docstrings + # 'sphinx.ext.autosummary', # Create neat summary tables +] + +autodoc2_packages = [ + "../pymerlin", +] + +autodoc2_docstring_parser_regexes = [ + # this will render all docstrings as Markdown + (r".*", "myst"), +] + +autodoc2_index_template = """ +API Reference +============= + +Here, you can find the details of functions, classes, and data structures provided by the `pymerlin` package. + +.. toctree:: + :titlesonly: +{% for package in top_level %} + {{ package }} +{%- endfor %} +""" + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'furo' +html_static_path = ['_static'] + +copyright = "2023 California Institute of Technology" diff --git a/docs/contribute.md b/docs/contribute.md new file mode 100644 index 0000000..9a18da8 --- /dev/null +++ b/docs/contribute.md @@ -0,0 +1,114 @@ +# Contributing + +We would love for you to contribute to pymerlin and help make it even better than it is today! As a contributor, here are the guidelines we would like you to follow: + +- [Question or Problem?](#question) +- [Building pymerlin](#building) +- [Pull Request Guidelines](#pr-guidelines) +- [Good Commit, PR, and Code Review Practices](#best-practices) +- [Submitting a Pull Request](#submit-pr) + +(question)= +## Got a Question or Problem? + +If you would like to chat about the question in real-time, you can reach out via [the Aerie users Slack channel](https://join.slack.com/t/nasa-ammos/shared_invite/zt-1mlgmk5c2-MgqVSyKzVRUWrXy87FNqPw). + +(building)= +## Building pymerlin + +To build and develop pymerlin please read through the [developer documentation](./developer.md). + +(pr-guidelines)= +## Pull Request Guidelines + +Here are some general Pull Request (PR) guidelines for the pymerlin project: + +- Every PR should include a summary of changes that gives reviewers an idea of what they should pay attention to. +- PR branches should have as "clean" of a history as possible. +- Each commit should present one change or idea to a reviewer. +- Commits that merely "fix up" previous commits should be interactively rebased and squashed into their targets. +- Prefer the use of `git rebase` over `git merge`: + + - `git rebase` actually _rebases_ your branch from the current development branch's endpoint. This localizes conflicts to the commits at which they actually appear, though it can become complicated when there are more than a few conflicts. + - `git merge` pulls in all the updates that your branch does not have, and combines them with the updates you have made in a single merge commit. This allows you to deal with any and all conflicts at once, but information such as when conflicts originated is lost. + + For more info on `git merge` vs `git rebase` see [here](https://www.atlassian.com/git/tutorials/merging-vs-rebasing). + +- Before merging a PR, the following requirements must be met. These requirements ensure that history is effectively linear, which aids readability and makes `git bisect` more useful and easier to reason about. + - At least one (preferably two) reviewers have approved the PR. + - No outstanding review comments have been left unresolved. + - The branch passes continuous integration. + - The branch has been rebased onto the current `develop` branch. +- The "Squash and merge" and "Rebase and merge" buttons on GitHub's PR interface should not be used. Always use the "Merge" strategy. + - In combination with the restrictions above, this ensures that features are neatly bracketed by merge commits on either side, making a clear hierarchical separation between features added to `develop` and the work that went into each feature. + +(best-practices)= +## Good Commit, PR, and Code Review Practices + +The pymerlin project relies on the ability to effectively query the Git history. Please read through the following resources before contributing: + +- [How to write a good commit message](https://chris.beams.io/posts/git-commit/) +- [Telling Stories Through Your Commits](https://blog.mocoso.co.uk/talks/2015/01/12/telling-stories-through-your-commits/) +- [How to Make Your Code Reviewer Fall in Love with You](https://mtlynch.io/code-review-love/) + +(submit-pr)= +## Submitting a Pull Request + +Please follow these instructions when submitting a Pull Request: + +1. Search [GitHub](https://github.com/NASA-AMMOS/pymerlin/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate effort. +1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design up front helps to ensure that we're ready to accept your work. +1. Clone the [pymerlin repo](https://github.com/NASA-AMMOS/pymerlin). +1. Make your changes in a new git branch: + + ```sh + git checkout develop + git pull origin develop + git checkout -b my-fix-branch develop + ``` + +1. Create your patch. +1. Commit your changes using a descriptive commit message that follows our [commit conventions](#best-practices). + + ```sh + git commit -a + ``` + + Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. + +1. Push your branch to GitHub: + + ```sh + git push origin my-fix-branch + ``` + +1. In GitHub, send a pull request to `pymerlin:develop`. +1. If we suggest changes then: + +- Make the required updates. +- [Rebase your branch](https://dev.to/maxwell_dev/the-git-rebase-introduction-i-wish-id-had) and force push to your branch to GitHub (this will update your Pull Request): + + ```sh + git rebase develop -i + git push -f + ``` + +After your pull request is merged, you can safely delete your branch and pull the changes from the repository: + +- Check out the develop branch: + + ```shell + git checkout develop + ``` + +- Update your develop with the latest version: + + ```shell + git pull origin develop + ``` + +- Delete the local branch: + + ```shell + git branch -D my-fix-branch + ``` diff --git a/docs/developer.md b/docs/developer.md new file mode 100644 index 0000000..2de51b1 --- /dev/null +++ b/docs/developer.md @@ -0,0 +1 @@ +# Developing pymerlin \ No newline at end of file diff --git a/docs/documentation.md b/docs/documentation.md new file mode 100644 index 0000000..5459e90 --- /dev/null +++ b/docs/documentation.md @@ -0,0 +1,14 @@ +# Documentation + +Contributions to this documentation are welcome! + +This documentation lives in the `docs` directory, and is built +by running the following: + +```shell +cd docs +make html +``` + +This documentation is built using [sphinx](https://www.sphinx-doc.org/en/master/) with the [MyST](https://mystmd.org/) +plugin, using the [Furo](https://pradyunsg.me/furo/) theme. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c500b39 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +--- +hide-toc: true +--- + +# pymerlin + +pymerlin is a discrete event simulation framework, built for use in the [Aerie](https://github.com/NASA-AMMOS/aerie>) ecosystem. + +To learn more about Aerie, read the [Aerie Docs](https://nasa-ammos.github.io/aerie-docs). + +Ready to get started? Check out the [Quickstart](./0_quickstart.md) guide. + +```{include} ../README.md +:start-after: +:end-before: +``` + +```{toctree} +:hidden: + +0_quickstart +1_tutorials/index +2_guides/index +3_explanation/index +apidocs/index +``` + +```{toctree} +:caption: Development +:hidden: + +architecture +developer +contribute +install +license +publishing +``` + + \ No newline at end of file diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..6f0fefb --- /dev/null +++ b/docs/install.md @@ -0,0 +1,3 @@ +# Install + +content goes here \ No newline at end of file diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..4ace915 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,4 @@ +# License + +```{include} ../LICENSE +``` \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000..8558c24 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,3 @@ +# Publishing to pypi + +content goes here \ No newline at end of file diff --git a/index_bak.rst b/index_bak.rst new file mode 100644 index 0000000..54128f3 --- /dev/null +++ b/index_bak.rst @@ -0,0 +1,40 @@ +pymerlin |release| +================= + +pymerlin is a discrete event simulation framework, built for use in the `Aerie `_ ecosystem. + +To learn more about Aerie, read the `Aerie Docs `_ + +Ready to get started? Check out the :doc:`Quickstart` guide. + +Source code +----------- + +You can access the source code at: https://github.com/mattdailis/pymerlin + +How to get help, contribute, or provide feedback +------------------------------------------------ + +See our :doc:`feedback and contribution submission guidelines `. + +Documentation +------------- + +.. toctree:: + :maxdepth: 2 + + quickstart + install + architecture + contribute + subfolder/index + +.. Links + +.. _Python: https://www.python.org/ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Markdown: https://daringfireball.net/projects/markdown/ +.. _Jinja2: https://palletsprojects.com/p/jinja/ +.. _`Pelican documentation`: https://docs.getpelican.com/latest/ +.. _`Pelican's internals`: https://docs.getpelican.com/en/latest/internals.html +.. _`Pelican Plugins`: https://github.com/pelican-plugins diff --git a/pymerlin/__init__.py b/pymerlin/__init__.py new file mode 100644 index 0000000..96cadab --- /dev/null +++ b/pymerlin/__init__.py @@ -0,0 +1,7 @@ +""" +This is the pymerlin module, doing pymerlin things +""" + +from ._internal._decorators import MissionModel +from ._internal._framework import simulate, Span +from ._internal._schedule import Schedule, Directive \ No newline at end of file diff --git a/pymerlin/_internal/README.md b/pymerlin/_internal/README.md new file mode 100644 index 0000000..b6b0426 --- /dev/null +++ b/pymerlin/_internal/README.md @@ -0,0 +1,4 @@ +# pymerlin internal + +Files in this package encapsulate details of py4j, such that (ideally) +none leak out to user-facing code. \ No newline at end of file diff --git a/pymerlin/_internal/_cell_type.py b/pymerlin/_internal/_cell_type.py new file mode 100644 index 0000000..e5db09c --- /dev/null +++ b/pymerlin/_internal/_cell_type.py @@ -0,0 +1,28 @@ +from pymerlin._internal._effect_trait import EffectTrait + + +class CellType: + def __init__(self, gateway): + self.gateway = gateway + + def getEffectType(self): + """ + + :return: EffectTrait + """ + return EffectTrait() + + def duplicate(self, state): + return self.gateway.jvm.org.apache.commons.lang3.mutable.MutableObject(state.getValue()) + + def apply(self, state, effect): + state.setValue(effect) + + def step(self, state, duration): + pass + + def getExpiry(self, state): + return self.gateway.jvm.java.util.Optional.empty() + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.CellType"] \ No newline at end of file diff --git a/pymerlin/_internal/_condition.py b/pymerlin/_internal/_condition.py new file mode 100644 index 0000000..50ed450 --- /dev/null +++ b/pymerlin/_internal/_condition.py @@ -0,0 +1,21 @@ +from pymerlin import model_actions +from pymerlin._internal._querier_adapter import QuerierAdapter + + +class Condition: + def __init__(self, gateway, func): + """ + func should return True or False (for now...) + """ + self.gateway = gateway + self.func = func + + def nextSatisfied(self, querier, horizon): + with model_actions._context(QuerierAdapter(querier)): + if self.func(): + return self.gateway.jvm.java.util.Optional.of(self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO) + else: + return self.gateway.jvm.java.util.Optional.empty() # Optional + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Condition"] diff --git a/pymerlin/decorators.py b/pymerlin/_internal/_decorators.py similarity index 50% rename from pymerlin/decorators.py rename to pymerlin/_internal/_decorators.py index 4494b14..ac685e8 100644 --- a/pymerlin/decorators.py +++ b/pymerlin/_internal/_decorators.py @@ -1,6 +1,22 @@ +""" +Provide the MissionModel decorator, which generates the .ActivityType decorators on the decorated class. +""" + +import inspect import warnings +from pymerlin._internal._task_specification import TaskSpecification + + def MissionModel(cls): + """ + Decorate a class + :param cls: + :return: + """ + if not inspect.isclass(cls): + warnings.warn("@MissionModel decorator is intended to be used on classes") + cls.activity_types = {} def inner(func): @@ -11,19 +27,4 @@ def inner(*args, **kwargs): return TaskSpecification(func, args, kwargs) return inner cls.ActivityType = inner - return cls - -class TaskSpecification: - def __init__(self, func, args, kwargs): - self.func = func - self.args = args - self.kwargs = kwargs - - def instantiate(self): - return self.func(*self.args, **self.kwargs) - - def __repr__(self): - return f"{self.func.__name__}({self.args}, {self.kwargs})" - - def __call__(self, *args, **kwargs): - return self.instantiate() \ No newline at end of file + return cls \ No newline at end of file diff --git a/pymerlin/_internal/_directive_type.py b/pymerlin/_internal/_directive_type.py new file mode 100644 index 0000000..619e34f --- /dev/null +++ b/pymerlin/_internal/_directive_type.py @@ -0,0 +1,24 @@ +from pymerlin._internal._globals import models_by_id +from pymerlin._internal._input_type import InputType +from pymerlin._internal._task import Task +from pymerlin._internal._task_factory import TaskFactory + + +class DirectiveType: + def __init__(self, gateway, activity, input_topic, output_topic): + self.gateway = gateway + self.activity = activity + self.input_topic = input_topic + self.output_topic = output_topic + + def getInputType(self): + return InputType() + + def getOutputType(self): + return None + + def getTaskFactory(self, model_id, args): + return TaskFactory(lambda: Task(self.gateway, models_by_id[model_id], self.activity, self.input_topic, self.output_topic)) + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType"] diff --git a/pymerlin/_internal/_effect_trait.py b/pymerlin/_internal/_effect_trait.py new file mode 100644 index 0000000..1eba73d --- /dev/null +++ b/pymerlin/_internal/_effect_trait.py @@ -0,0 +1,9 @@ +class EffectTrait: + def empty(self): + return 0 + def sequentially(self, prefix, suffix): + return suffix + def concurrently(self, left, right): + return 0 + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait"] diff --git a/pymerlin/_internal/_framework.py b/pymerlin/_internal/_framework.py new file mode 100644 index 0000000..5369071 --- /dev/null +++ b/pymerlin/_internal/_framework.py @@ -0,0 +1,104 @@ +import os +from collections import namedtuple + +from py4j.java_gateway import Py4JJavaError, get_field + +from pymerlin._internal._gateway import start_gateway +from pymerlin._internal._model_type import ModelType +from pymerlin._internal._py4j_utilities import make_array +from pymerlin._internal._serialized_value import from_serialized_value +from pymerlin.duration import Duration, MICROSECONDS + + +class Consumer: + def __init__(self, f): + self.f = f + + def accept(self, args): + self.f.__call__(args) + + class Java: + implements = ["java.util.function.Consumer"] + + +def make_schedule(gateway, schedule): + entry_list = [] + for offset, directive in schedule.entries: + entry_list.append(gateway.jvm.org.apache.commons.lang3.tuple.Pair.of( + gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND.times(offset.micros), + gateway.jvm.gov.nasa.ammos.aerie.merlin.python.Directive( + directive.type, + gateway.jvm.java.util.Map.of()))) + return gateway.jvm.gov.nasa.ammos.aerie.merlin.python.Schedule.build( + make_array( + gateway, + gateway.jvm.org.apache.commons.lang3.tuple.Pair, + entry_list) + ) + + +def simulate_helper(gateway, model_type, config, schedule, duration): + valid_types = set(model_type.getDirectiveTypes().keys()) + for offset, directive in schedule.entries: + if directive.type not in valid_types: + raise Exception("Unknown activity type: " + directive.type) + merlin = gateway.entry_point.getMerlin() + if type(duration) is str: + duration = Duration.from_string(duration) + duration = gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS.times(duration.micros) + start_time = gateway.jvm.java.time.Instant.EPOCH + schedule = make_schedule(gateway, schedule) + return merlin.simulate(model_type, config, schedule, start_time, duration) + + +def simulate(model_type, schedule, duration): + if not type(model_type) == ModelType: + model_type = ModelType(model_type) + jar_path = os.path.join(os.path.dirname(__file__), "jars", "pymerlin.jar") + with start_gateway(jar_path) as gateway: + try: + model_type.set_gateway(gateway) + config = None + results = simulate_helper(gateway, model_type, config, schedule, duration) + profiles, spans, events = unpack_simulation_results(gateway, results) + return profiles, spans, events + + except Py4JJavaError as e: + e.java_exception.printStackTrace() + # TODO extract all info from java exception and raise a purely python exception + raise e + + +ProfileSegment = namedtuple("ProfileSegment", "extent dynamics") +Span = namedtuple("Span", "type start duration") + +def unpack_simulation_results(gateway, results): + start_time = unix_micros_from_instant(get_field(results, 'startTime')) + + profiles = {} + for profile_name, profile_segments in get_field(results, 'discreteProfiles').items(): + profiles[profile_name] = [ProfileSegment( + Duration.of(x.extent().dividedBy(gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND), + MICROSECONDS), + from_serialized_value(gateway, x.dynamics())) for x in profile_segments.getRight()] + + spans = [] + for activity_id, activity in get_field(results, 'simulatedActivities').items(): + spans.append( + Span(activity.type(), Duration.of(unix_micros_from_instant(activity.start()) - start_time, MICROSECONDS), + Duration.of(activity.duration().dividedBy( + gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND), MICROSECONDS))) + for activity_id, activity in get_field(results, 'unfinishedActivities').items(): + spans.append( + Span(activity.type(), Duration.of(unix_micros_from_instant(activity.start()) - start_time, MICROSECONDS), + None)) + + events = [] + + return profiles, spans, events + + +def unix_micros_from_instant(instant): + seconds = instant.getEpochSecond() + nanos = instant.getNano() + return int((seconds * 1_000_000) + (nanos / 1000)) \ No newline at end of file diff --git a/pymerlin/gateway.py b/pymerlin/_internal/_gateway.py similarity index 96% rename from pymerlin/gateway.py rename to pymerlin/_internal/_gateway.py index 63ba735..47d1466 100644 --- a/pymerlin/gateway.py +++ b/pymerlin/_internal/_gateway.py @@ -13,6 +13,7 @@ @contextmanager def start_gateway(jar_path, java_executable='java'): + print("Starting gateway at " + jar_path) process = subprocess.Popen( [java_executable, '-jar', jar_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/pymerlin/_internal/_globals.py b/pymerlin/_internal/_globals.py new file mode 100644 index 0000000..dd96e98 --- /dev/null +++ b/pymerlin/_internal/_globals.py @@ -0,0 +1,6 @@ +models_by_id = {} + +_current_context = [None, None] +_yield_callback = [] + +_future = [None] diff --git a/pymerlin/_internal/_input_type.py b/pymerlin/_internal/_input_type.py new file mode 100644 index 0000000..f94a49e --- /dev/null +++ b/pymerlin/_internal/_input_type.py @@ -0,0 +1,18 @@ +class InputType: + def getParameters(self): + return [] + + def getRequiredParameters(self): + return [] + + def instantiate(self, args): + return None + + def getArguments(self, val): + return {} + + def getValidationFailures(self, val): + return [] + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.InputType"] diff --git a/pymerlin/_internal/_model_type.py b/pymerlin/_internal/_model_type.py new file mode 100644 index 0000000..fedb46c --- /dev/null +++ b/pymerlin/_internal/_model_type.py @@ -0,0 +1,68 @@ +from py4j.java_collections import MapConverter + +from pymerlin._internal._cell_type import CellType +from pymerlin._internal._directive_type import DirectiveType +from pymerlin._internal._globals import models_by_id +from pymerlin._internal._output_type import OutputType +from pymerlin._internal._registrar import Registrar +from pymerlin._internal._resource import Resource + + +class ModelType: + def __init__(self, model_class): + self.gateway = None + self.model_class = model_class + self.raw_activity_types = model_class.activity_types + self.activity_types = [] + + def set_gateway(self, gateway): + self.gateway = gateway + self.activity_types = [] + for activity in self.raw_activity_types.values(): + self.activity_types.append((activity, gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic(), + gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic())) + + def instantiate(self, start_time, config, builder): + cell_type = CellType(self.gateway) + + registrar = Registrar() + model = self.model_class(registrar) + + for cell_ref, initial_value in registrar.cells: + topic = self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic() + cell_id = builder.allocate(self.gateway.jvm.org.apache.commons.lang3.mutable.MutableObject(initial_value), + cell_type, self.gateway.jvm.java.util.function.Function.identity(), topic) + cell_ref.id = cell_id + cell_ref.topic = topic + + for activity, input_topic, output_topic in self.activity_types: + activity_type_name = activity.__name__ + builder.topic(f"ActivityType.Input.{activity_type_name}", input_topic, OutputType(self.gateway)) + builder.topic(f"ActivityType.Output.{activity_type_name}", output_topic, OutputType(self.gateway)) + + for resource_name, resource_func in registrar.resources: + builder.resource(resource_name, Resource(self.gateway, resource_func)) + + models_by_id[id(model)] = model, self + return id(model) + + def getDirectiveTypes(self): + return MapConverter().convert( + { + activity_type[0].__name__: DirectiveType( + self.gateway, + activity_type[0], # function + activity_type[1], # input_topic + activity_type[2]) # output_topic + for activity_type in self.activity_types + }, + self.gateway._gateway_client) + + def getConfigurationType(self): + pass + + def toString(self): + return str(self) + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.ModelType"] \ No newline at end of file diff --git a/pymerlin/_internal/_output_type.py b/pymerlin/_internal/_output_type.py new file mode 100644 index 0000000..50cd37c --- /dev/null +++ b/pymerlin/_internal/_output_type.py @@ -0,0 +1,18 @@ +from pymerlin._internal._serialized_value import to_serialized_value + + +class OutputType: + def __init__(self, gateway): + self.gateway = gateway + + def getSchema(self): + pass # TODO return ValueSchema + + def serialize(self, value): + return to_serialized_value(self.gateway, value) + + def hashCode(self): + return 0 + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.OutputType"] diff --git a/pymerlin/plot.py b/pymerlin/_internal/_plot.py similarity index 100% rename from pymerlin/plot.py rename to pymerlin/_internal/_plot.py diff --git a/pymerlin/py4j_utilities.py b/pymerlin/_internal/_py4j_utilities.py similarity index 100% rename from pymerlin/py4j_utilities.py rename to pymerlin/_internal/_py4j_utilities.py diff --git a/pymerlin/_internal/_querier_adapter.py b/pymerlin/_internal/_querier_adapter.py new file mode 100644 index 0000000..2debe39 --- /dev/null +++ b/pymerlin/_internal/_querier_adapter.py @@ -0,0 +1,6 @@ +class QuerierAdapter: + def __init__(self, querier): + self.querier = querier + + def get(self, cell_id): + return self.querier.getState(cell_id) diff --git a/pymerlin/_internal/_registrar.py b/pymerlin/_internal/_registrar.py new file mode 100644 index 0000000..9a8a4be --- /dev/null +++ b/pymerlin/_internal/_registrar.py @@ -0,0 +1,39 @@ +from pymerlin import model_actions + + +class Registrar: + def __init__(self): + self.cells = [] + self.resources = [] + self.topics = [] + + def cell(self, initial_value): + ref = CellRef() + self.cells.append((ref, initial_value)) + return ref + + def resource(self, name, f): + """ + Declare a resource to track + :param name: The name of the resource + :param f: A function to calculate the resource, or a cell that contains the value of the resource + """ + if not callable(f): + cell = f + f = cell.get + self.resources.append((name, f)) + + def topic(self, name): + pass + + +class CellRef: + def __init__(self): + self.id = None + self.topic = None + + def emit(self, event): + model_actions._current_context[0].emit(event, self.topic) + + def get(self): + return model_actions._current_context[0].get(self.id).getValue() diff --git a/pymerlin/_internal/_resource.py b/pymerlin/_internal/_resource.py new file mode 100644 index 0000000..6e82d66 --- /dev/null +++ b/pymerlin/_internal/_resource.py @@ -0,0 +1,22 @@ +from pymerlin import model_actions +from pymerlin._internal._output_type import OutputType +from pymerlin._internal._querier_adapter import QuerierAdapter + + +class Resource: + def __init__(self, gateway, resource_func): + self.gateway = gateway + self.func = resource_func + + def getType(self): + return "discrete" + + def getOutputType(self): + return OutputType(self.gateway) + + def getDynamics(self, querier): + with model_actions._context(QuerierAdapter(querier)): + return self.func() + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Resource"] diff --git a/pymerlin/schedule.py b/pymerlin/_internal/_schedule.py similarity index 100% rename from pymerlin/schedule.py rename to pymerlin/_internal/_schedule.py diff --git a/pymerlin/_internal/_serialized_value.py b/pymerlin/_internal/_serialized_value.py new file mode 100644 index 0000000..cff839f --- /dev/null +++ b/pymerlin/_internal/_serialized_value.py @@ -0,0 +1,50 @@ +from py4j.java_collections import MapConverter, ListConverter, JavaMap, JavaList + +def from_serialized_value(gateway, value): + return value.match(SerializedValueVisitor(gateway)) + + +class SerializedValueVisitor: + def __init__(self, gateway): + self.gateway = gateway + + def onNull(self, value): + return None + + def onNumeric(self, value): + return float(value) + + def onBoolean(self, value): + return bool(value) + + def onString(self, value): + return str(value) + + def onMap(self, value): + return {k: from_serialized_value(self.gateway, v) for k, v in value.items()} + + def onList(self, value): + return [from_serialized_value(self.gateway, v) for v in value] + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue$Visitor"] + + +def to_serialized_value(gateway, value): + if type(value) is str: + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) + if type(value) is int: + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) + if type(value) is float: + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) + if type(value) is dict or type(value) is JavaMap: + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of( + MapConverter().convert({ + k: to_serialized_value(gateway, v) for k, v in value.items() + }, gateway._gateway_client)) + if type(value) is list or type(value) is JavaList: + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of( + ListConverter().convert([ + to_serialized_value(gateway, v) for v in value + ], gateway._gateway_client)) + raise NotImplementedError(value) \ No newline at end of file diff --git a/pymerlin/_internal/_task.py b/pymerlin/_internal/_task.py new file mode 100644 index 0000000..899bd9f --- /dev/null +++ b/pymerlin/_internal/_task.py @@ -0,0 +1,143 @@ +import asyncio + +from pymerlin import model_actions +from pymerlin._internal._condition import Condition +from pymerlin._internal._task_factory import TaskFactory +from pymerlin._internal._task_specification import TaskSpecification +from pymerlin.model_actions import Completed, Delayed, Awaiting, Calling + +from pymerlin._internal._globals import _future + + +class Task: + def __init__(self, gateway, model, activity, input_topic=None, output_topic=None): + self.gateway = gateway + self.model, self.model_type = model + self.activity = activity + self.input_topic = input_topic + self.output_topic = output_topic + self.continuation = None + self.task_handle = None + self.loop = None + + def step(self, scheduler): + def spawn(child: TaskSpecification): + new_task = Task(self.gateway, (self.model, self.model_type), child, *get_topics(self.model_type, child.func)) + scheduler.spawn(self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.InSpan.Fresh, TaskFactory(lambda: new_task)) + with model_actions._context(scheduler, spawn): + if self.continuation is None: + self.loop = asyncio.new_event_loop() + if self.input_topic is not None: + scheduler.emit({}, self.input_topic) + task_handle, future, done_callback = run_task(self.loop, self.activity, self.model) + if self.output_topic is not None: + scheduler.emit("doesn't matter", self.output_topic) + self.task_handle = task_handle + + else: + future = resume_task(self.loop, self.task_handle, self.continuation) + _future[0] = future + self.loop.run_until_complete(future) + result, continuation = future.result() + self.continuation = continuation + + if type(result) == Completed: + return TaskStatus.completed(self.gateway, result.value) + if type(result) == Delayed: + return TaskStatus.delayed(self.gateway, + self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.of( + result.duration.micros, + self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS), + self) + if type(result) == Awaiting: + return TaskStatus.awaiting(self.gateway, Condition(self.gateway, result.condition), self) + if type(result) == Calling: + new_task = Task(self.gateway, (self.model, self.model_type), result.child, *get_topics(self.model_type, result.child.func)) + return TaskStatus.calling(self.gateway, self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.InSpan.Fresh, + TaskFactory(lambda: new_task), self) + raise Exception("Invalid response from task") + + def release(self): + if self.loop is not None: + self.task_handle.cancel() + self.loop.run_until_complete(self.exit()) + + async def exit(self): + self.loop.stop() + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Task"] + +def get_topics(model_type, func): + for activity_func, input_topic, output_topic in model_type.activity_types: + if activity_func is func: + return input_topic, output_topic + return None, None + +def run_task(loop, task, model): + future = loop.create_future() + task_handle = loop.create_task(propagate_exception(lambda: task.__call__(model))) + done_callback = on_task_finish(future) + task_handle.add_done_callback(done_callback) + model_actions._set_yield_callback(on_task_yield(future, task_handle, done_callback)) + return task_handle, future, done_callback + + +def resume_task(loop, task_handle, continuation): + future = loop.create_future() + done_callback = on_task_finish(future) + task_handle.add_done_callback(done_callback) + model_actions._set_yield_callback(on_task_yield(future, task_handle, done_callback)) + continuation.set_result("ignored") + return future + + +def on_task_finish(future): + def inner(fut): + try: + future.set_result((Completed(fut.result()), "finished")) + except asyncio.CancelledError: + pass + finally: + model_actions._clear_context() + model_actions._clear_yield_callback() + + return inner + + +def on_task_yield(future, task_handle, done_callback): + def inner(y, continuation): + try: + future.set_result((y, continuation)) + task_handle.remove_done_callback(done_callback) + finally: + model_actions._clear_context() # Catch if activity forgets to await + model_actions._clear_yield_callback() + + return inner + + + +class TaskStatus: + @staticmethod + def completed(gateway, value): + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.completed(value) + + @staticmethod + def delayed(gateway, duration, continuation): + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.delayed(duration, continuation) + + @staticmethod + def calling(gateway, child_span, child, continuation): + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.calling(child_span, child, continuation) + + @staticmethod + def awaiting(gateway, condition: "Condition", continuation): + return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.awaiting(condition, continuation) + + +async def propagate_exception(f): + try: + return await f() + except Exception as e: + _future[0].set_exception(e) \ No newline at end of file diff --git a/pymerlin/_internal/_task_factory.py b/pymerlin/_internal/_task_factory.py new file mode 100644 index 0000000..1e5f108 --- /dev/null +++ b/pymerlin/_internal/_task_factory.py @@ -0,0 +1,9 @@ +class TaskFactory: + def __init__(self, task_factory): + self.task_factory = task_factory + + def create(self, executor): + return self.task_factory.__call__() + + class Java: + implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory"] diff --git a/pymerlin/_internal/_task_specification.py b/pymerlin/_internal/_task_specification.py new file mode 100644 index 0000000..a9db733 --- /dev/null +++ b/pymerlin/_internal/_task_specification.py @@ -0,0 +1,14 @@ +class TaskSpecification: + def __init__(self, func, args, kwargs): + self.func = func + self.args = args + self.kwargs = kwargs + + def instantiate(self): + return self.func(*self.args, **self.kwargs) + + def __repr__(self): + return f"{self.func.__name__}({self.args}, {self.kwargs})" + + def __call__(self, *args, **kwargs): + return self.instantiate() diff --git a/pymerlin.jar b/pymerlin/_internal/jars/pymerlin.jar similarity index 100% rename from pymerlin.jar rename to pymerlin/_internal/jars/pymerlin.jar diff --git a/pymerlin/duration.py b/pymerlin/duration.py index 62efb51..55702f4 100644 --- a/pymerlin/duration.py +++ b/pymerlin/duration.py @@ -1,4 +1,72 @@ class Duration: + """ + A signed measure of the temporal distance between two instants. + + Durations are constructed by measuring a quantity of the provided units, such as {@link #SECOND} and {@link #HOUR}. + This can be done in multiple ways: + + - Use the static factory method {@link Duration#of}, e.g. {@code Duration.of(5, SECONDS} + - Use the static function {@link #duration}, e.g. {@code duration(5, SECONDS)} + - Multiply an existing duration by a quantity, e.g. {@code SECOND.times(5)} + + Durations are internally represented as a fixed-point data type. The fixed-point representation preserves + accuracy unlike floating-point arithmetic, which becomes less precise further from zero. Preserving accuracy is + necessary in the domain of discrete event simulation. To ensure that causal/temporal orderings between timed + elements in a simulation are preserved, it is necessary that operations on time quantities are exact. Arithmetic + between fixed-point representations preserves such temporal orderings. + + The internal fixed-point representation of a Duration is a {@code long}. This representation is evident in + the API where creating Durations and performing arithmetic operations often take a {@code long} as a parameter. + As a result, any custom unit such as {@code DAY = duration(24, HOURS)} can be used directly with other + Merlin-provided units like {@code SECONDS}.

+ + A time value is represented as a {@code long} where an increment maps to number a specific time unit. Currently, + the underlying time unit is microseconds, however, one should not rely on this always being the case. The maximum + value of a fixed-point type is simply the largest value that can be represented by the underlying integer type. + For a {@code long} this yields a range of (-2^63) to (2^63 - 1), or almost 600,000 years, at microsecond + resolution. + + Note that derived units such as DAY, WEEK, MONTH, and YEAR are not included, because their values + depend on properties of the particular calendrical system in use. For example: + + - The notion of "day" depends on the astronomical system against which time is measured. + For example, the synodic (solar) day and the sidereal day are distinguished by which celestial body is held fixed + in the sky by the passage of a day. (Synodic time fixes the body being orbited around; sidereal time + fixes the far field of stars.) + + - The notion of "year" has precisely the same problem, with a similar synodic/sidereal distinction. + + - +

+ The notion of "month" is worse, in that it depends on the presence of a *tertiary* body whose sygyzies with the + other two bodies delimit integer quantities of the unit. (A syzygy is a collinear configuration of the bodies.) + The lunar calendar (traditionally used in China) is based on a combination of lunar and solar + synodic quantities. ("Month" derives from "moon".) +

+ +

+ The month of the Gregorian calendar is approximately a lunar synodic month, except that the definition was + intentionally de-regularized (including intercalary days) in deference to the Earth's solar year. + (Other calendars even invoke days *outside of any month*, which Wikipedia claims are called "epagomenal days".) + * In retrospect, it is unsurprising that ISO 8601 ordinal dates drop the month altogether, + * since "month" is a (complicated) derived notion in the Gregorian calendar. + *

+ * + *
  • + * The notion of "week" seemingly has no basis in the symmetries of celestial bodies, and is instead a derived unit. + * Unfortunately, not only is it fundamentally based on the notion of "day", different calendars assign a different + * number of days to the span of a week. + + If you are working within the Gregorian calendar, the standard `java.time` package has you covered. + + If you are working with spacecraft, you may need to separate concepts such as "Earth day" and "Martian day", which + are synodic periods measured against the Sun but from different bodies. Worse, you likely need to convert between + such reference systems frequently, with a great deal of latitude in the choice of bodies being referenced. + The gold standard is the well-known SPICE toolkit, coupled with a good set of ephemerides and clock kernels. + + If you're just looking for a rough estimate, you can define 24-hour days and 7-day weeks and 30-day months + within your own domain in terms of the precise units we give here. + """ def __init__(self, micros): self.micros = micros @@ -103,6 +171,9 @@ def __le__(self, other): Duration.HOUR = Duration.MINUTE.times(60) Duration.HOURS = Duration.HOUR +""" +No time at all +""" ZERO = Duration.ZERO MICROSECOND = Duration.MICROSECOND MICROSECONDS = Duration.MICROSECONDS diff --git a/pymerlin/framework.py b/pymerlin/framework.py deleted file mode 100644 index 4a8b8a1..0000000 --- a/pymerlin/framework.py +++ /dev/null @@ -1,549 +0,0 @@ -import asyncio -import os -from collections import namedtuple -from contextlib import contextmanager - -from py4j.java_collections import MapConverter, ListConverter, JavaMap, JavaList - -from pymerlin import model_actions -from pymerlin.decorators import TaskSpecification -from pymerlin.duration import Duration, MICROSECONDS -from pymerlin.gateway import start_gateway -from pymerlin.model_actions import Completed, Delayed, Awaiting, Calling -from pymerlin.py4j_utilities import make_array - -from py4j.java_gateway import Py4JJavaError, get_field - -models_by_id = {} - - -class CellRef: - def __init__(self): - self.id = None - self.topic = None - - def emit(self, event): - model_actions._context[0].emit(event, self.topic) - - def get(self): - return model_actions._context[0].get(self.id).getValue() - - -class Registrar: - def __init__(self): - self.cells = [] - self.resources = [] - self.topics = [] - - def cell(self, initial_value): - ref = CellRef() - self.cells.append((ref, initial_value)) - return ref - - def resource(self, name, f): - """ - Declare a resource to track - :param name: The name of the resource - :param f: A function to calculate the resource, or a cell that contains the value of the resource - """ - if not callable(f): - cell = f - f = cell.get - self.resources.append((name, f)) - - def topic(self, name): - pass - - -class ModelType: - def __init__(self, model_class): - self.gateway = None - self.model_class = model_class - self.raw_activity_types = model_class.activity_types - self.activity_types = [] - - def set_gateway(self, gateway): - self.gateway = gateway - self.activity_types = [] - for activity in self.raw_activity_types.values(): - self.activity_types.append((activity, gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic(), - gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic())) - - def instantiate(self, start_time, config, builder): - cell_type = CellType(self.gateway) - - registrar = Registrar() - model = self.model_class(registrar) - - for cell_ref, initial_value in registrar.cells: - topic = self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.driver.Topic() - cell_id = builder.allocate(self.gateway.jvm.org.apache.commons.lang3.mutable.MutableObject(initial_value), - cell_type, self.gateway.jvm.java.util.function.Function.identity(), topic) - cell_ref.id = cell_id - cell_ref.topic = topic - - for activity, input_topic, output_topic in self.activity_types: - activity_type_name = activity.__name__ - builder.topic(f"ActivityType.Input.{activity_type_name}", input_topic, OutputType(self.gateway)) - builder.topic(f"ActivityType.Output.{activity_type_name}", output_topic, OutputType(self.gateway)) - - for resource_name, resource_func in registrar.resources: - builder.resource(resource_name, Resource(self.gateway, resource_func)) - - models_by_id[id(model)] = model, self - return id(model) - - def getDirectiveTypes(self): - return MapConverter().convert( - { - activity_type[0].__name__: DirectiveType( - self.gateway, - activity_type[0], # function - activity_type[1], # input_topic - activity_type[2]) # output_topic - for activity_type in self.activity_types - }, - self.gateway._gateway_client) - - def getConfigurationType(self): - pass - - def toString(self): - return str(self) - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.ModelType"] - - -class Resource: - def __init__(self, gateway, resource_func): - self.gateway = gateway - self.func = resource_func - - def getType(self): - return "discrete" - - def getOutputType(self): - return OutputType(self.gateway) - - def getDynamics(self, querier): - with model_actions.context(QuerierAdapter(querier)): - return self.func() - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Resource"] - - -class QuerierAdapter: - def __init__(self, querier): - self.querier = querier - - def get(self, cell_id): - return self.querier.getState(cell_id) - - -class CellType: - def __init__(self, gateway): - self.gateway = gateway - - def getEffectType(self): - """ - - :return: EffectTrait - """ - return EffectTrait() - - def duplicate(self, state): - return self.gateway.jvm.org.apache.commons.lang3.mutable.MutableObject(state.getValue()) - - def apply(self, state, effect): - state.setValue(effect) - - def step(self, state, duration): - pass - - def getExpiry(self, state): - return self.gateway.jvm.java.util.Optional.empty() - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.CellType"] - - -class EffectTrait: - def empty(self): - return 0 - def sequentially(self, prefix, suffix): - return suffix - def concurrently(self, left, right): - return 0 - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait"] - - -class InputType: - def getParameters(self): - return [] - - def getRequiredParameters(self): - return [] - - def instantiate(self, args): - return None - - def getArguments(self, val): - return {} - - def getValidationFailures(self, val): - return [] - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.InputType"] - - -def to_serialized_value(gateway, value): - if type(value) is str: - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) - if type(value) is int: - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) - if type(value) is float: - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of(value) - if type(value) is dict or type(value) is JavaMap: - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of( - MapConverter().convert({ - k: to_serialized_value(gateway, v) for k, v in value.items() - }, gateway._gateway_client)) - if type(value) is list or type(value) is JavaList: - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.of( - ListConverter().convert([ - to_serialized_value(gateway, v) for v in value - ], gateway._gateway_client)) - raise NotImplementedError(value) - - -def from_serialized_value(gateway, value): - return value.match(SerializedValueVisitor(gateway)) - - -class SerializedValueVisitor: - def __init__(self, gateway): - self.gateway = gateway - - def onNull(self, value): - return None - - def onNumeric(self, value): - return float(value) - - def onBoolean(self, value): - return bool(value) - - def onString(self, value): - return str(value) - - def onMap(self, value): - return {k: from_serialized_value(self.gateway, v) for k, v in value.items()} - - def onList(self, value): - return [from_serialized_value(self.gateway, v) for v in value] - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue$Visitor"] - - -class OutputType: - def __init__(self, gateway): - self.gateway = gateway - - def getSchema(self): - pass # TODO return ValueSchema - - def serialize(self, value): - return to_serialized_value(self.gateway, value) - - def hashCode(self): - return 0 - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.OutputType"] - - -class Consumer: - def __init__(self, f): - self.f = f - - def accept(self, args): - self.f.__call__(args) - - class Java: - implements = ["java.util.function.Consumer"] - - -class DirectiveType: - def __init__(self, gateway, activity, input_topic, output_topic): - self.gateway = gateway - self.activity = activity - self.input_topic = input_topic - self.output_topic = output_topic - - def getInputType(self): - return InputType() - - def getOutputType(self): - return None - - def getTaskFactory(self, model_id, args): - return TaskFactory(lambda: Task(self.gateway, models_by_id[model_id], self.activity, self.input_topic, self.output_topic)) - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType"] - - -class TaskFactory: - def __init__(self, task_factory): - self.task_factory = task_factory - - def create(self, executor): - return self.task_factory.__call__() - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory"] - - -_future = [None] - -async def propagate_exception(f): - try: - return await f() - except Exception as e: - _future[0].set_exception(e) - -class Task: - def __init__(self, gateway, model, activity, input_topic=None, output_topic=None): - self.gateway = gateway - self.model, self.model_type = model - self.activity = activity - self.input_topic = input_topic - self.output_topic = output_topic - self.continuation = None - self.task_handle = None - self.loop = None - - def step(self, scheduler): - def spawn(child: TaskSpecification): - new_task = Task(self.gateway, (self.model, self.model_type), child, *get_topics(self.model_type, child.func)) - scheduler.spawn(self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.InSpan.Fresh, TaskFactory(lambda: new_task)) - with model_actions.context(scheduler, spawn): - if self.continuation is None: - self.loop = asyncio.new_event_loop() - if self.input_topic is not None: - scheduler.emit({}, self.input_topic) - task_handle, future, done_callback = run_task(self.loop, self.activity, self.model) - if self.output_topic is not None: - scheduler.emit("doesn't matter", self.output_topic) - self.task_handle = task_handle - - else: - future = resume_task(self.loop, self.task_handle, self.continuation) - _future[0] = future - self.loop.run_until_complete(future) - result, continuation = future.result() - self.continuation = continuation - - if type(result) == Completed: - return TaskStatus.completed(self.gateway, result.value) - if type(result) == Delayed: - return TaskStatus.delayed(self.gateway, - self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.of( - result.duration.micros, - self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS), - self) - if type(result) == Awaiting: - return TaskStatus.awaiting(self.gateway, Condition(self.gateway, result.condition), self) - if type(result) == Calling: - new_task = Task(self.gateway, (self.model, self.model_type), result.child, *get_topics(self.model_type, result.child.func)) - return TaskStatus.calling(self.gateway, self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.InSpan.Fresh, - TaskFactory(lambda: new_task), self) - raise Exception("Invalid response from task") - - def release(self): - if self.loop is not None: - self.task_handle.cancel() - self.loop.run_until_complete(self.exit()) - - async def exit(self): - self.loop.stop() - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Task"] - - -def get_topics(model_type: ModelType, func): - for activity_func, input_topic, output_topic in model_type.activity_types: - if activity_func is func: - return input_topic, output_topic - return None, None - - - - -def on_task_finish(future): - def inner(fut): - try: - future.set_result((Completed(fut.result()), "finished")) - except asyncio.CancelledError: - pass - finally: - model_actions.clear_context() - model_actions.clear_yield_callback() - - return inner - - -def on_task_yield(future, task_handle, done_callback): - def inner(y, continuation): - try: - future.set_result((y, continuation)) - task_handle.remove_done_callback(done_callback) - finally: - model_actions.clear_context() # Catch if activity forgets to await - model_actions.clear_yield_callback() - - return inner - - -def run_task(loop, task, model): - future = loop.create_future() - task_handle = loop.create_task(propagate_exception(lambda: task.__call__(model))) - done_callback = on_task_finish(future) - task_handle.add_done_callback(done_callback) - model_actions.set_yield_callback(on_task_yield(future, task_handle, done_callback)) - return task_handle, future, done_callback - - -def resume_task(loop, task_handle, continuation): - future = loop.create_future() - done_callback = on_task_finish(future) - task_handle.add_done_callback(done_callback) - model_actions.set_yield_callback(on_task_yield(future, task_handle, done_callback)) - continuation.set_result("ignored") - return future - - -class TaskStatus: - @staticmethod - def completed(gateway, value): - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.completed(value) - - @staticmethod - def delayed(gateway, duration, continuation): - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.delayed(duration, continuation) - - @staticmethod - def calling(gateway, child_span, child, continuation): - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.calling(child_span, child, continuation) - - @staticmethod - def awaiting(gateway, condition: "Condition", continuation): - return gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus.awaiting(condition, continuation) - - -class Condition: - def __init__(self, gateway, func): - """ - func should return True or False (for now...) - """ - self.gateway = gateway - self.func = func - - def nextSatisfied(self, querier, horizon): - with model_actions.context(QuerierAdapter(querier)): - if self.func(): - return self.gateway.jvm.java.util.Optional.of(self.gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO) - else: - return self.gateway.jvm.java.util.Optional.empty() # Optional - - class Java: - implements = ["gov.nasa.jpl.aerie.merlin.protocol.model.Condition"] - - -def make_schedule(gateway, schedule): - entry_list = [] - for offset, directive in schedule.entries: - entry_list.append(gateway.jvm.org.apache.commons.lang3.tuple.Pair.of( - gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND.times(offset.micros), - gateway.jvm.gov.nasa.ammos.aerie.merlin.python.Directive( - directive.type, - gateway.jvm.java.util.Map.of()))) - return gateway.jvm.gov.nasa.ammos.aerie.merlin.python.Schedule.build( - make_array( - gateway, - gateway.jvm.org.apache.commons.lang3.tuple.Pair, - entry_list) - ) - - -def simulate_helper(gateway, model_type, config, schedule, duration): - valid_types = set(model_type.getDirectiveTypes().keys()) - for offset, directive in schedule.entries: - if directive.type not in valid_types: - raise Exception("Unknown activity type: " + directive.type) - merlin = gateway.entry_point.getMerlin() - if type(duration) is str: - duration = Duration.from_string(duration) - duration = gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS.times(duration.micros) - start_time = gateway.jvm.java.time.Instant.EPOCH - schedule = make_schedule(gateway, schedule) - return merlin.simulate(model_type, config, schedule, start_time, duration) - - -def simulate(model_type, schedule, duration): - if not type(model_type) == ModelType: - model_type = ModelType(model_type) - with start_gateway(os.path.join(os.path.dirname(__file__), '../pymerlin.jar')) as gateway: - try: - model_type.set_gateway(gateway) - config = None - results = simulate_helper(gateway, model_type, config, schedule, duration) - profiles, spans, events = unpack_simulation_results(gateway, results) - return profiles, spans, events - - except Py4JJavaError as e: - e.java_exception.printStackTrace() - # TODO extract all info from java exception and raise a purely python exception - raise e - - -ProfileSegment = namedtuple("ProfileSegment", "extent dynamics") -Span = namedtuple("Span", "type start duration") - - -def unpack_simulation_results(gateway, results): - start_time = unix_micros_from_instant(get_field(results, 'startTime')) - - profiles = {} - for profile_name, profile_segments in get_field(results, 'discreteProfiles').items(): - profiles[profile_name] = [ProfileSegment( - Duration.of(x.extent().dividedBy(gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND), - MICROSECONDS), - from_serialized_value(gateway, x.dynamics())) for x in profile_segments.getRight()] - - spans = [] - for activity_id, activity in get_field(results, 'simulatedActivities').items(): - spans.append( - Span(activity.type(), Duration.of(unix_micros_from_instant(activity.start()) - start_time, MICROSECONDS), - Duration.of(activity.duration().dividedBy( - gateway.jvm.gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND), MICROSECONDS))) - for activity_id, activity in get_field(results, 'unfinishedActivities').items(): - spans.append( - Span(activity.type(), Duration.of(unix_micros_from_instant(activity.start()) - start_time, MICROSECONDS), - None)) - - events = [] - - return profiles, spans, events - - -def unix_micros_from_instant(instant): - seconds = instant.getEpochSecond() - nanos = instant.getNano() - return int((seconds * 1_000_000) + (nanos / 1000)) \ No newline at end of file diff --git a/pymerlin/model_actions.py b/pymerlin/model_actions.py index 17214f9..545cca1 100644 --- a/pymerlin/model_actions.py +++ b/pymerlin/model_actions.py @@ -1,11 +1,16 @@ +""" +This module provides actions that tasks can take. Certain actions are labeled `async`, which means they must be called +with the `await` keyword - for example, `await delay("01:00:00")` +""" + + import asyncio from collections import namedtuple from contextlib import contextmanager from pymerlin.duration import Duration -_context = [None, None] -_yield_callback = [] +from pymerlin._internal._globals import _current_context, _yield_callback Completed = namedtuple("Completed", "value") @@ -15,29 +20,29 @@ @contextmanager -def context(scheduler, spawner=None): - set_context(scheduler, spawner) +def _context(scheduler, spawner=None): + _set_context(scheduler, spawner) yield - clear_context() + _clear_context() -def set_context(context, spawner): - _context.clear() - _context.append(context) - _context.append(spawner) +def _set_context(context, spawner): + _current_context.clear() + _current_context.append(context) + _current_context.append(spawner) -def clear_context(): - _context.clear() - _context.append(None) - _context.append(None) +def _clear_context(): + _current_context.clear() + _current_context.append(None) + _current_context.append(None) -def set_yield_callback(callback): +def _set_yield_callback(callback): _yield_callback.clear() _yield_callback.append(callback) -def clear_yield_callback(): +def _clear_yield_callback(): _yield_callback.clear() _yield_callback.append(None) @@ -55,7 +60,7 @@ def spawn(child): :param coro: :return: """ - _context[1](child) + _current_context[1](child) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..048636c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pymerlin" +version = "0.0.2" +dependencies = [ + "bokeh>=3.4.2", + "numpy>=2.0.0", + "py4j>=0.10.9.7" +] +requires-python = ">=3.6" +authors = [ + {name = "Matthew Dailis", email = "matthew.l.dailis@jpl.nasa.gov"}, +] +maintainers = [ +] +description = "Python mission modeling framework for Aerie" +readme = "README.md" +license = {file = "LICENSE"} +keywords = ["aerie", "merlin", "simulation", "discrete event"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Programming Language :: Python" +] + +[project.optional-dependencies] +#gui = ["PyQt5"] +#cli = [ +# "rich", +# "click", +#] +#spice = ["spiceypy"] + +[project.urls] +Homepage = "https://example.com" +Documentation = "https://readthedocs.org" +Repository = "https://github.com/mattdailis/pymerlin" +"Bug Tracker" = "https://github.com/mattdailis/pymerlin/issues" +Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" + +#[project.scripts] +#spam-cli = "spam:main_cli" +# +#[project.gui-scripts] +#spam-gui = "spam:main_gui" +# +#[project.entry-points."spam.magical"] +#tomatoes = "spam:main_tomatoes" + +[tool.hatch.build.targets.sdist] +include = [ + "pymerlin", +] +exclude = [ + "java", "docs", "tests" +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d36b378..749372f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,110 +1,3 @@ -anyio==4.4.0 -appnope==0.1.4 -argon2-cffi==23.1.0 -argon2-cffi-bindings==21.2.0 -arrow==1.3.0 -asttokens==2.4.1 -async-lru==2.0.4 -attrs==23.2.0 -Babel==2.15.0 -beautifulsoup4==4.12.3 -bleach==6.1.0 bokeh==3.4.2 -certifi==2024.6.2 -cffi==1.16.0 -charset-normalizer==3.3.2 -comm==0.2.2 -contourpy==1.2.1 -debugpy==1.8.2 -decorator==5.1.1 -defusedxml==0.7.1 -exceptiongroup==1.2.1 -executing==2.0.1 -fastjsonschema==2.20.0 -fqdn==1.5.1 -h11==0.14.0 -httpcore==1.0.5 -httpx==0.27.0 -idna==3.7 -importlib_metadata==8.0.0 -iniconfig==2.0.0 -ipykernel==6.29.4 -ipython==8.18.1 -ipywidgets==8.1.3 -isoduration==20.11.0 -jedi==0.19.1 -Jinja2==3.1.4 -json5==0.9.25 -jsonpointer==3.0.0 -jsonschema==4.22.0 -jsonschema-specifications==2023.12.1 -jupyter-events==0.10.0 -jupyter-lsp==2.2.5 -jupyter_bokeh==4.0.5 -jupyter_client==8.6.2 -jupyter_core==5.7.2 -jupyter_server==2.14.1 -jupyter_server_terminals==0.5.3 -jupyterlab==4.2.2 -jupyterlab_pygments==0.3.0 -jupyterlab_server==2.27.2 -jupyterlab_widgets==3.0.11 -MarkupSafe==2.1.5 -matplotlib-inline==0.1.7 -mistune==3.0.2 -nbclient==0.10.0 -nbconvert==7.16.4 -nbformat==5.10.4 -nest-asyncio==1.6.0 -notebook_shim==0.2.4 numpy==2.0.0 -overrides==7.7.0 -packaging==24.1 -pandas==2.2.2 -pandocfilters==1.5.1 -parso==0.8.4 -pexpect==4.9.0 -pillow==10.3.0 -platformdirs==4.2.2 -pluggy==1.5.0 -prometheus_client==0.20.0 -prompt_toolkit==3.0.47 -psutil==6.0.0 -ptyprocess==0.7.0 -pure-eval==0.2.2 py4j==0.10.9.7 -pycparser==2.22 -Pygments==2.18.0 -pytest==8.2.2 -python-dateutil==2.9.0.post0 -python-json-logger==2.0.7 -pytz==2024.1 -PyYAML==6.0.1 -pyzmq==26.0.3 -referencing==0.35.1 -requests==2.32.3 -rfc3339-validator==0.1.4 -rfc3986-validator==0.1.1 -rpds-py==0.18.1 -Send2Trash==1.8.3 -six==1.16.0 -sniffio==1.3.1 -soupsieve==2.5 -stack-data==0.6.3 -terminado==0.18.1 -tinycss2==1.3.0 -tomli==2.0.1 -tornado==6.4.1 -traitlets==5.14.3 -types-python-dateutil==2.9.0.20240316 -typing_extensions==4.12.2 -tzdata==2024.1 -uri-template==1.3.0 -urllib3==2.2.2 -wcwidth==0.2.13 -webcolors==24.6.0 -webencodings==0.5.1 -websocket-client==1.8.0 -widgetsnbextension==4.0.11 -xyzservices==2024.6.0 -zipp==3.19.2 diff --git a/tests/test.py b/tests/test.py index 7bbb7a1..ef8ccda 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,10 +1,10 @@ import pytest -from pymerlin.decorators import MissionModel +from pymerlin import MissionModel from pymerlin.duration import Duration, SECONDS -from pymerlin.framework import simulate, Span +from pymerlin import simulate, Span from pymerlin.model_actions import delay, spawn, call, wait_until -from pymerlin.schedule import Schedule, Directive +from pymerlin import Schedule, Directive from py4j.java_gateway import Py4JJavaError