diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index 62756c1a8..4ceab5afd 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -66,7 +66,7 @@ jobs: - name: Build project dependencies run: | ./build_tools/build_install_deps_linux.sh - "${PYTHON_EXECUTABLE}" -m pip install "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" + "${PYTHON_EXECUTABLE}" -m pip install --prefer-binary "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" ##################################################################################### diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1fdcad88d..49eec3a1c 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -52,7 +52,7 @@ jobs: "${PYTHON_EXECUTABLE}" -m pip install --upgrade numpy "${PYTHON_EXECUTABLE}" -m pip install tensorflow "${PYTHON_EXECUTABLE}" -m pip install "torch==1.8.0+cpu" -f https://download.pytorch.org/whl/torch_stable.html - "${PYTHON_EXECUTABLE}" -m pip install "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" + "${PYTHON_EXECUTABLE}" -m pip install --prefer-binary "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" "${PYTHON_EXECUTABLE}" -m pip install "ray[default,rllib]<=1.4.0" # Type checking is not working with 1.4.1 ##################################################################################### @@ -121,6 +121,7 @@ jobs: --disable=fixme,abstract-method,protected-access,useless-super-delegation \ --disable=too-many-instance-attributes,too-many-arguments,too-few-public-methods,too-many-lines \ --disable=too-many-locals,too-many-branches,too-many-statements \ + --disable=unspecified-encoding,logging-fstring-interpolation \ --generated-members=numpy.*,torch.* "gym_jiminy/" mypy --allow-redefinition --check-untyped-defs --disallow-incomplete-defs --disallow-untyped-defs \ diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 4e8f73e83..3ed4e40bb 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -50,7 +50,7 @@ jobs: - name: Build project dependencies run: | python -m pip install "torch==1.8.0+cpu" -f https://download.pytorch.org/whl/torch_stable.html - python -m pip install "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" + python -m pip install --prefer-binary "gym>=0.18.3" "stable_baselines3>=0.10" "importlib-metadata>=3.3.0" & "./build_tools/build_install_deps_windows.ps1" ##################################################################################### diff --git a/CMakeLists.txt b/CMakeLists.txt index 97cc4dd51..bb9781a17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) # Set the build version -set(BUILD_VERSION 1.6.29) +set(BUILD_VERSION 1.6.30) # Set compatibility if(CMAKE_VERSION VERSION_GREATER "3.11.0") diff --git a/README.md b/README.md index dc92267d3..b3e22277c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Beside a strong focus on performance to answer machine learning's need for runni - C++ core with full python bindings, providing frontend API parity between both languages. - Designed with machine learning in mind, with seemless wrapping of robots as [OpenAI Gym](https://github.com/openai/gym) environments using one-liners. Jiminy provides both the physical engine and the robot model (including sensors) required for learning. - Easy to install: `pip` is all that is needed to [get you started](#getting-started) ! -- Dedicated integration in jupyter notebook working out-of-the-box - including 3D rendering using [Meshcat](https://github.com/rdeits/MeshCat.jl). This facilitates working on remote headless environnement such as machine learning clusters. +- Dedicated integration in Google Colab, Jupyter Lab, and VSCode working out-of-the-box - including interactive 3D viewer based on [Meshcat](https://github.com/rdeits/MeshCat.jl). This facilitates working on remote headless environnement such as machine learning clusters. - Cross-platform offscreen rendering capability, without requiring X-server, based on [Panda3d](https://github.com/panda3d/panda3d). - Rich simulation log output, easily customizable for recording, introspection and debugging. The simulation log is made available in RAM directly for fast access, and can be exported in raw binary, CSV or [HDF5](https://portal.hdfgroup.org/display/HDF5/Introduction+to+HDF5) format. - Available for both Linux and Windows platforms. diff --git a/core/src/engine/EngineMultiRobot.cc b/core/src/engine/EngineMultiRobot.cc index 96dc33cea..c428b7914 100644 --- a/core/src/engine/EngineMultiRobot.cc +++ b/core/src/engine/EngineMultiRobot.cc @@ -1171,9 +1171,9 @@ namespace jiminy { stepper_ = std::unique_ptr( new RungeKuttaDOPRIStepper(systemOde, - robots, - engineOptions_->stepper.tolAbs, - engineOptions_->stepper.tolRel)); + robots, + engineOptions_->stepper.tolAbs, + engineOptions_->stepper.tolRel)); } else if (engineOptions_->stepper.odeSolver == "runge_kutta_4") { @@ -2920,6 +2920,9 @@ namespace jiminy { constraint->reset(q, v); constraint->enable(); + auto & frameConstraint = static_cast(*constraint.get()); + vector3_t & positionRef = frameConstraint.getReferenceTransform().translation(); + positionRef.noalias() -= depth * nGround; } } } @@ -2996,11 +2999,15 @@ namespace jiminy } else { - // Enable fixed frame constraint and reset it if it was disable + // Enable fixed frame constraint and reset it if it was disable, + // then move the reference position at the surface of the ground. if (!constraint->getIsEnabled()) { constraint->reset(q, v); constraint->enable(); + auto & frameConstraint = static_cast(*constraint.get()); + vector3_t & positionRef = frameConstraint.getReferenceTransform().translation(); + positionRef.noalias() -= depth * nGround; } } } @@ -3047,19 +3054,19 @@ namespace jiminy // Compute normal force float64_t const fextNormal = - std::min(contactOptions_.stiffness * depth + contactOptions_.damping * vDepth, 0.0); - fextInWorld = fextNormal * nGround; + fextInWorld.noalias() = fextNormal * nGround; // Compute friction forces vector3_t const vTangential = vContactInWorld - vDepth * nGround; float64_t const vRatio = std::min(vTangential.norm() / contactOptions_.transitionVelocity, 1.0); float64_t const fextTangential = contactOptions_.friction * vRatio * fextNormal; - fextInWorld -= fextTangential * vTangential; + fextInWorld.noalias() -= fextTangential * vTangential; // Add blending factor if (contactOptions_.transitionEps > EPS) { - float64_t const blendingFactor = -depth / contactOptions_.transitionEps; - float64_t const blendingLaw = std::tanh(2 * blendingFactor); + float64_t const blendingFactor = - depth / contactOptions_.transitionEps; + float64_t const blendingLaw = std::tanh(2.0 * blendingFactor); fextInWorld *= blendingLaw; } } @@ -3378,7 +3385,7 @@ namespace jiminy vectorN_t const & stiffness = mdlDynOptions.flexibilityConfig[i].stiffness; vectorN_t const & damping = mdlDynOptions.flexibilityConfig[i].damping; - quaternion_t const quat(q.segment<4>(positionIdx).data()); // Only way to initialize with [x,y,z,w] order + quaternion_t const quat(q.segment<4>(positionIdx)); // Only way to initialize with [x,y,z,w] order vectorN_t const axis = pinocchio::quaternion::log3(quat); uInternal.segment<3>(velocityIdx).array() += - stiffness.array() * axis.array() @@ -3756,7 +3763,7 @@ namespace jiminy i, pinocchio::LOCAL, jointJacobian); - uAugmented += jointJacobian.transpose() * fext[i].toVector(); + uAugmented.noalias() += jointJacobian.transpose() * fext[i].toVector(); } // Compute non-linear effects @@ -3823,7 +3830,7 @@ namespace jiminy // Convert the force from local world aligned to local frame frameIndex_t const & frameIdx = frameConstraint.getFrameIdx(); pinocchio::SE3 const & transformContactInWorld = data.oMf[frameIdx]; - forceIt->linear() = transformContactInWorld.rotation().transpose() * fextWorld; + forceIt->linear().noalias() = transformContactInWorld.rotation().transpose() * fextWorld; // Convert the force from local world aligned to local parent joint jointIndex_t const & jointIdx = model.frames[frameIdx].parent; diff --git a/core/src/solver/LCPSolvers.cc b/core/src/solver/LCPSolvers.cc index 8e264e2e3..db05aa836 100644 --- a/core/src/solver/LCPSolvers.cc +++ b/core/src/solver/LCPSolvers.cc @@ -89,14 +89,38 @@ namespace jiminy https://github.com/dartsim/dart/blob/master/dart/constraint/PgsBoxedLcpSolver.cpp */ assert(b.size() > 0 && "The number of inequality constraints must be larger than 0."); - /* Reset shuffling counter. + /* Adapt shuffling indices if the number of indices has changed. Note that it may converge faster to enforce constraints in reverse order, since usually constraints bounds dependending on others have lower indices by design. For instance, for friction, x and y */ - indices_.resize(b.size()); - std::generate(indices_.begin(), indices_.end(), - [n = static_cast(indices_.size() - 1)]() mutable { return n--; }); - lastShuffle_ = 0U; // Do NOT shuffle indices right after initialization + size_t const nIndicesOrig = indices_.size(); + size_t const nIndices = b.size(); + if (nIndicesOrig < nIndices) + { + indices_.resize(nIndices); + std::generate(indices_.begin() + nIndicesOrig, indices_.end(), + [n = static_cast(nIndices - 1)]() mutable { return n--; }); + } + else if (nIndicesOrig > nIndices) + { + size_t shiftIdx = nIndices; + for (size_t i = 0; i < nIndices; ++i) + { + if (static_cast(indices_[i]) >= nIndices) + { + for (size_t j = shiftIdx; j < nIndicesOrig; ++j) + { + ++shiftIdx; + if (static_cast(indices_[j]) < nIndices) + { + indices_[i] = indices_[j]; + break; + } + } + } + } + indices_.resize(nIndices); + } // Normalizing for (Eigen::Index i = 0; i < b.size(); ++i) @@ -111,6 +135,8 @@ namespace jiminy bool_t isSuccess = ProjectedGaussSeidelIter(A, b, lo, hi, fIdx, false, true, x); if (isSuccess) { + // Do NOT shuffle indices unless necessary to avoid discontinuities + lastShuffle_ = 0U; return true; } } diff --git a/examples/tutorial.ipynb b/examples/tutorial.ipynb index 4ee437e82..ae4062cb8 100644 --- a/examples/tutorial.ipynb +++ b/examples/tutorial.ipynb @@ -2,7 +2,6 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, "source": [ "# An introduction to Jiminy\n", "\n", @@ -27,37 +26,12 @@ "The robot is constructed from a URDF - this builds a jiminy.Model object - but extra information needs to be provided as well for a simulation: which sensors to use and what are their caracteristic ? Which joints have a motor attached and what are its properties ? What are the contact points with the ground (if any) ? All this are informations gathered to build a full robot.\n", "\n", "So let's get our first example running: we set the inverted pendulum away from its upward position and watch it fall." - ] + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 2, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2cca83eadffa4a86904a50015cb21fff", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(FloatProgress(value=0.0, max=10.0), HTML(value='')))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], + "execution_count": 1, "source": [ "import os\n", "from pkg_resources import resource_filename\n", @@ -98,998 +72,101 @@ "\n", "# Launch the simulation\n", "simulator.simulate(simulation_duration, q0, v0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The simulation generates a log of its comuptation: this log can be retrieved by using ```simulator.get_log``` - and written to a file for latter processing by the engine with ```simulator.engine.write_log```." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, + ], "outputs": [ { + "output_type": "display_data", "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "/* global mpl */\n", - "window.mpl = {};\n", - "\n", - "mpl.get_websocket_type = function () {\n", - " if (typeof WebSocket !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof MozWebSocket !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert(\n", - " 'Your browser does not have WebSocket support. ' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.'\n", - " );\n", - " }\n", - "};\n", - "\n", - "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = this.ws.binaryType !== undefined;\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById('mpl-warnings');\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent =\n", - " 'This browser does not support binary websocket messages. ' +\n", - " 'Performance may be slow.';\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = document.createElement('div');\n", - " this.root.setAttribute('style', 'display: inline-block');\n", - " this._root_extra_style(this.root);\n", - "\n", - " parent_element.appendChild(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message('supports_binary', { value: fig.supports_binary });\n", - " fig.send_message('send_image_mode', {});\n", - " if (mpl.ratio !== 1) {\n", - " fig.send_message('set_dpi_ratio', { dpi_ratio: mpl.ratio });\n", - " }\n", - " fig.send_message('refresh', {});\n", - " };\n", - "\n", - " this.imageObj.onload = function () {\n", - " if (fig.image_mode === 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function () {\n", - " fig.ws.close();\n", - " };\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "};\n", - "\n", - "mpl.figure.prototype._init_header = function () {\n", - " var titlebar = document.createElement('div');\n", - " titlebar.classList =\n", - " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", - " var titletext = document.createElement('div');\n", - " titletext.classList = 'ui-dialog-title';\n", - " titletext.setAttribute(\n", - " 'style',\n", - " 'width: 100%; text-align: center; padding: 3px;'\n", - " );\n", - " titlebar.appendChild(titletext);\n", - " this.root.appendChild(titlebar);\n", - " this.header = titletext;\n", - "};\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", - "\n", - "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", - "\n", - "mpl.figure.prototype._init_canvas = function () {\n", - " var fig = this;\n", - "\n", - " var canvas_div = (this.canvas_div = document.createElement('div'));\n", - " canvas_div.setAttribute(\n", - " 'style',\n", - " 'border: 1px solid #ddd;' +\n", - " 'box-sizing: content-box;' +\n", - " 'clear: both;' +\n", - " 'min-height: 1px;' +\n", - " 'min-width: 1px;' +\n", - " 'outline: 0;' +\n", - " 'overflow: hidden;' +\n", - " 'position: relative;' +\n", - " 'resize: both;'\n", - " );\n", - "\n", - " function on_keyboard_event_closure(name) {\n", - " return function (event) {\n", - " return fig.key_event(event, name);\n", - " };\n", - " }\n", - "\n", - " canvas_div.addEventListener(\n", - " 'keydown',\n", - " on_keyboard_event_closure('key_press')\n", - " );\n", - " canvas_div.addEventListener(\n", - " 'keyup',\n", - " on_keyboard_event_closure('key_release')\n", - " );\n", - "\n", - " this._canvas_extra_style(canvas_div);\n", - " this.root.appendChild(canvas_div);\n", - "\n", - " var canvas = (this.canvas = document.createElement('canvas'));\n", - " canvas.classList.add('mpl-canvas');\n", - " canvas.setAttribute('style', 'box-sizing: content-box;');\n", - "\n", - " this.context = canvas.getContext('2d');\n", - "\n", - " var backingStore =\n", - " this.context.backingStorePixelRatio ||\n", - " this.context.webkitBackingStorePixelRatio ||\n", - " this.context.mozBackingStorePixelRatio ||\n", - " this.context.msBackingStorePixelRatio ||\n", - " this.context.oBackingStorePixelRatio ||\n", - " this.context.backingStorePixelRatio ||\n", - " 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", - " 'canvas'\n", - " ));\n", - " rubberband_canvas.setAttribute(\n", - " 'style',\n", - " 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", - " );\n", - "\n", - " var resizeObserver = new ResizeObserver(function (entries) {\n", - " var nentries = entries.length;\n", - " for (var i = 0; i < nentries; i++) {\n", - " var entry = entries[i];\n", - " var width, height;\n", - " if (entry.contentBoxSize) {\n", - " if (entry.contentBoxSize instanceof Array) {\n", - " // Chrome 84 implements new version of spec.\n", - " width = entry.contentBoxSize[0].inlineSize;\n", - " height = entry.contentBoxSize[0].blockSize;\n", - " } else {\n", - " // Firefox implements old version of spec.\n", - " width = entry.contentBoxSize.inlineSize;\n", - " height = entry.contentBoxSize.blockSize;\n", - " }\n", - " } else {\n", - " // Chrome <84 implements even older version of spec.\n", - " width = entry.contentRect.width;\n", - " height = entry.contentRect.height;\n", - " }\n", - "\n", - " // Keep the size of the canvas and rubber band canvas in sync with\n", - " // the canvas container.\n", - " if (entry.devicePixelContentBoxSize) {\n", - " // Chrome 84 implements new version of spec.\n", - " canvas.setAttribute(\n", - " 'width',\n", - " entry.devicePixelContentBoxSize[0].inlineSize\n", - " );\n", - " canvas.setAttribute(\n", - " 'height',\n", - " entry.devicePixelContentBoxSize[0].blockSize\n", - " );\n", - " } else {\n", - " canvas.setAttribute('width', width * mpl.ratio);\n", - " canvas.setAttribute('height', height * mpl.ratio);\n", - " }\n", - " canvas.setAttribute(\n", - " 'style',\n", - " 'width: ' + width + 'px; height: ' + height + 'px;'\n", - " );\n", - "\n", - " rubberband_canvas.setAttribute('width', width);\n", - " rubberband_canvas.setAttribute('height', height);\n", - "\n", - " // And update the size in Python. We ignore the initial 0/0 size\n", - " // that occurs as the element is placed into the DOM, which should\n", - " // otherwise not happen due to the minimum size styling.\n", - " if (width != 0 && height != 0) {\n", - " fig.request_resize(width, height);\n", - " }\n", - " }\n", - " });\n", - " resizeObserver.observe(canvas_div);\n", - "\n", - " function on_mouse_event_closure(name) {\n", - " return function (event) {\n", - " return fig.mouse_event(event, name);\n", - " };\n", - " }\n", - "\n", - " rubberband_canvas.addEventListener(\n", - " 'mousedown',\n", - " on_mouse_event_closure('button_press')\n", - " );\n", - " rubberband_canvas.addEventListener(\n", - " 'mouseup',\n", - " on_mouse_event_closure('button_release')\n", - " );\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband_canvas.addEventListener(\n", - " 'mousemove',\n", - " on_mouse_event_closure('motion_notify')\n", - " );\n", - "\n", - " rubberband_canvas.addEventListener(\n", - " 'mouseenter',\n", - " on_mouse_event_closure('figure_enter')\n", - " );\n", - " rubberband_canvas.addEventListener(\n", - " 'mouseleave',\n", - " on_mouse_event_closure('figure_leave')\n", - " );\n", - "\n", - " canvas_div.addEventListener('wheel', function (event) {\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " on_mouse_event_closure('scroll')(event);\n", - " });\n", - "\n", - " canvas_div.appendChild(canvas);\n", - " canvas_div.appendChild(rubberband_canvas);\n", - "\n", - " this.rubberband_context = rubberband_canvas.getContext('2d');\n", - " this.rubberband_context.strokeStyle = '#000000';\n", - "\n", - " this._resize_canvas = function (width, height, forward) {\n", - " if (forward) {\n", - " canvas_div.style.width = width + 'px';\n", - " canvas_div.style.height = height + 'px';\n", - " }\n", - " };\n", - "\n", - " // Disable right mouse context menu.\n", - " this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", - " event.preventDefault();\n", - " return false;\n", - " });\n", - "\n", - " function set_focus() {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "};\n", - "\n", - "mpl.figure.prototype._init_toolbar = function () {\n", - " var fig = this;\n", - "\n", - " var toolbar = document.createElement('div');\n", - " toolbar.classList = 'mpl-toolbar';\n", - " this.root.appendChild(toolbar);\n", - "\n", - " function on_click_closure(name) {\n", - " return function (_event) {\n", - " return fig.toolbar_button_onclick(name);\n", - " };\n", - " }\n", - "\n", - " function on_mouseover_closure(tooltip) {\n", - " return function (event) {\n", - " if (!event.currentTarget.disabled) {\n", - " return fig.toolbar_button_onmouseover(tooltip);\n", - " }\n", - " };\n", - " }\n", - "\n", - " fig.buttons = {};\n", - " var buttonGroup = document.createElement('div');\n", - " buttonGroup.classList = 'mpl-button-group';\n", - " for (var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " /* Instead of a spacer, we start a new button group. */\n", - " if (buttonGroup.hasChildNodes()) {\n", - " toolbar.appendChild(buttonGroup);\n", - " }\n", - " buttonGroup = document.createElement('div');\n", - " buttonGroup.classList = 'mpl-button-group';\n", - " continue;\n", - " }\n", - "\n", - " var button = (fig.buttons[name] = document.createElement('button'));\n", - " button.classList = 'mpl-widget';\n", - " button.setAttribute('role', 'button');\n", - " button.setAttribute('aria-disabled', 'false');\n", - " button.addEventListener('click', on_click_closure(method_name));\n", - " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", - "\n", - " var icon_img = document.createElement('img');\n", - " icon_img.src = '_images/' + image + '.png';\n", - " icon_img.srcset = '_images/' + image + '_large.png 2x';\n", - " icon_img.alt = tooltip;\n", - " button.appendChild(icon_img);\n", - "\n", - " buttonGroup.appendChild(button);\n", - " }\n", - "\n", - " if (buttonGroup.hasChildNodes()) {\n", - " toolbar.appendChild(buttonGroup);\n", - " }\n", - "\n", - " var fmt_picker = document.createElement('select');\n", - " fmt_picker.classList = 'mpl-widget';\n", - " toolbar.appendChild(fmt_picker);\n", - " this.format_dropdown = fmt_picker;\n", - "\n", - " for (var ind in mpl.extensions) {\n", - " var fmt = mpl.extensions[ind];\n", - " var option = document.createElement('option');\n", - " option.selected = fmt === mpl.default_extension;\n", - " option.innerHTML = fmt;\n", - " fmt_picker.appendChild(option);\n", - " }\n", - "\n", - " var status_bar = document.createElement('span');\n", - " status_bar.classList = 'mpl-message';\n", - " toolbar.appendChild(status_bar);\n", - " this.message = status_bar;\n", - "};\n", - "\n", - "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", - " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", - " // which will in turn request a refresh of the image.\n", - " this.send_message('resize', { width: x_pixels, height: y_pixels });\n", - "};\n", - "\n", - "mpl.figure.prototype.send_message = function (type, properties) {\n", - " properties['type'] = type;\n", - " properties['figure_id'] = this.id;\n", - " this.ws.send(JSON.stringify(properties));\n", - "};\n", - "\n", - "mpl.figure.prototype.send_draw_message = function () {\n", - " if (!this.waiting) {\n", - " this.waiting = true;\n", - " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", - " var format_dropdown = fig.format_dropdown;\n", - " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", - " fig.ondownload(fig, format);\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_resize = function (fig, msg) {\n", - " var size = msg['size'];\n", - " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", - " fig._resize_canvas(size[0], size[1], msg['forward']);\n", - " fig.send_message('refresh', {});\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", - " var x0 = msg['x0'] / mpl.ratio;\n", - " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n", - " var x1 = msg['x1'] / mpl.ratio;\n", - " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n", - " x0 = Math.floor(x0) + 0.5;\n", - " y0 = Math.floor(y0) + 0.5;\n", - " x1 = Math.floor(x1) + 0.5;\n", - " y1 = Math.floor(y1) + 0.5;\n", - " var min_x = Math.min(x0, x1);\n", - " var min_y = Math.min(y0, y1);\n", - " var width = Math.abs(x1 - x0);\n", - " var height = Math.abs(y1 - y0);\n", - "\n", - " fig.rubberband_context.clearRect(\n", - " 0,\n", - " 0,\n", - " fig.canvas.width / mpl.ratio,\n", - " fig.canvas.height / mpl.ratio\n", - " );\n", - "\n", - " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", - " // Updates the figure title.\n", - " fig.header.textContent = msg['label'];\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", - " var cursor = msg['cursor'];\n", - " switch (cursor) {\n", - " case 0:\n", - " cursor = 'pointer';\n", - " break;\n", - " case 1:\n", - " cursor = 'default';\n", - " break;\n", - " case 2:\n", - " cursor = 'crosshair';\n", - " break;\n", - " case 3:\n", - " cursor = 'move';\n", - " break;\n", - " }\n", - " fig.rubberband_canvas.style.cursor = cursor;\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_message = function (fig, msg) {\n", - " fig.message.textContent = msg['message'];\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", - " // Request the server to send over a new figure.\n", - " fig.send_draw_message();\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", - " fig.image_mode = msg['mode'];\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", - " for (var key in msg) {\n", - " if (!(key in fig.buttons)) {\n", - " continue;\n", - " }\n", - " fig.buttons[key].disabled = !msg[key];\n", - " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", - " if (msg['mode'] === 'PAN') {\n", - " fig.buttons['Pan'].classList.add('active');\n", - " fig.buttons['Zoom'].classList.remove('active');\n", - " } else if (msg['mode'] === 'ZOOM') {\n", - " fig.buttons['Pan'].classList.remove('active');\n", - " fig.buttons['Zoom'].classList.add('active');\n", - " } else {\n", - " fig.buttons['Pan'].classList.remove('active');\n", - " fig.buttons['Zoom'].classList.remove('active');\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.updated_canvas_event = function () {\n", - " // Called whenever the canvas gets updated.\n", - " this.send_message('ack', {});\n", - "};\n", - "\n", - "// A function to construct a web socket function for onmessage handling.\n", - "// Called in the figure constructor.\n", - "mpl.figure.prototype._make_on_message_function = function (fig) {\n", - " return function socket_on_message(evt) {\n", - " if (evt.data instanceof Blob) {\n", - " /* FIXME: We get \"Resource interpreted as Image but\n", - " * transferred with MIME type text/plain:\" errors on\n", - " * Chrome. But how to set the MIME type? It doesn't seem\n", - " * to be part of the websocket stream */\n", - " evt.data.type = 'image/png';\n", - "\n", - " /* Free the memory for the previous frames */\n", - " if (fig.imageObj.src) {\n", - " (window.URL || window.webkitURL).revokeObjectURL(\n", - " fig.imageObj.src\n", - " );\n", - " }\n", - "\n", - " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", - " evt.data\n", - " );\n", - " fig.updated_canvas_event();\n", - " fig.waiting = false;\n", - " return;\n", - " } else if (\n", - " typeof evt.data === 'string' &&\n", - " evt.data.slice(0, 21) === 'data:image/png;base64'\n", - " ) {\n", - " fig.imageObj.src = evt.data;\n", - " fig.updated_canvas_event();\n", - " fig.waiting = false;\n", - " return;\n", - " }\n", - "\n", - " var msg = JSON.parse(evt.data);\n", - " var msg_type = msg['type'];\n", - "\n", - " // Call the \"handle_{type}\" callback, which takes\n", - " // the figure and JSON message as its only arguments.\n", - " try {\n", - " var callback = fig['handle_' + msg_type];\n", - " } catch (e) {\n", - " console.log(\n", - " \"No handler for the '\" + msg_type + \"' message type: \",\n", - " msg\n", - " );\n", - " return;\n", - " }\n", - "\n", - " if (callback) {\n", - " try {\n", - " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", - " callback(fig, msg);\n", - " } catch (e) {\n", - " console.log(\n", - " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", - " e,\n", - " e.stack,\n", - " msg\n", - " );\n", - " }\n", - " }\n", - " };\n", - "};\n", - "\n", - "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", - "mpl.findpos = function (e) {\n", - " //this section is from http://www.quirksmode.org/js/events_properties.html\n", - " var targ;\n", - " if (!e) {\n", - " e = window.event;\n", - " }\n", - " if (e.target) {\n", - " targ = e.target;\n", - " } else if (e.srcElement) {\n", - " targ = e.srcElement;\n", - " }\n", - " if (targ.nodeType === 3) {\n", - " // defeat Safari bug\n", - " targ = targ.parentNode;\n", - " }\n", - "\n", - " // pageX,Y are the mouse positions relative to the document\n", - " var boundingRect = targ.getBoundingClientRect();\n", - " var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", - " var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", - "\n", - " return { x: x, y: y };\n", - "};\n", - "\n", - "/*\n", - " * return a copy of an object with only non-object keys\n", - " * we need this to avoid circular references\n", - " * http://stackoverflow.com/a/24161582/3208463\n", - " */\n", - "function simpleKeys(original) {\n", - " return Object.keys(original).reduce(function (obj, key) {\n", - " if (typeof original[key] !== 'object') {\n", - " obj[key] = original[key];\n", - " }\n", - " return obj;\n", - " }, {});\n", - "}\n", - "\n", - "mpl.figure.prototype.mouse_event = function (event, name) {\n", - " var canvas_pos = mpl.findpos(event);\n", - "\n", - " if (name === 'button_press') {\n", - " this.canvas.focus();\n", - " this.canvas_div.focus();\n", - " }\n", - "\n", - " var x = canvas_pos.x * mpl.ratio;\n", - " var y = canvas_pos.y * mpl.ratio;\n", - "\n", - " this.send_message(name, {\n", - " x: x,\n", - " y: y,\n", - " button: event.button,\n", - " step: event.step,\n", - " guiEvent: simpleKeys(event),\n", - " });\n", - "\n", - " /* This prevents the web browser from automatically changing to\n", - " * the text insertion cursor when the button is pressed. We want\n", - " * to control all of the cursor setting manually through the\n", - " * 'cursor' event from matplotlib */\n", - " event.preventDefault();\n", - " return false;\n", - "};\n", - "\n", - "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", - " // Handle any extra behaviour associated with a key event\n", - "};\n", - "\n", - "mpl.figure.prototype.key_event = function (event, name) {\n", - " // Prevent repeat events\n", - " if (name === 'key_press') {\n", - " if (event.which === this._key) {\n", - " return;\n", - " } else {\n", - " this._key = event.which;\n", - " }\n", - " }\n", - " if (name === 'key_release') {\n", - " this._key = null;\n", - " }\n", - "\n", - " var value = '';\n", - " if (event.ctrlKey && event.which !== 17) {\n", - " value += 'ctrl+';\n", - " }\n", - " if (event.altKey && event.which !== 18) {\n", - " value += 'alt+';\n", - " }\n", - " if (event.shiftKey && event.which !== 16) {\n", - " value += 'shift+';\n", - " }\n", - "\n", - " value += 'k';\n", - " value += event.which.toString();\n", - "\n", - " this._key_event_extra(event, name);\n", - "\n", - " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", - " return false;\n", - "};\n", - "\n", - "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", - " if (name === 'download') {\n", - " this.handle_save(this, null);\n", - " } else {\n", - " this.send_message('toolbar_button', { name: name });\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", - " this.message.textContent = tooltip;\n", - "};\n", - "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", - "\n", - "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", - "\n", - "mpl.default_extension = \"png\";/* global mpl */\n", - "\n", - "var comm_websocket_adapter = function (comm) {\n", - " // Create a \"websocket\"-like object which calls the given IPython comm\n", - " // object with the appropriate methods. Currently this is a non binary\n", - " // socket, so there is still some room for performance tuning.\n", - " var ws = {};\n", - "\n", - " ws.close = function () {\n", - " comm.close();\n", - " };\n", - " ws.send = function (m) {\n", - " //console.log('sending', m);\n", - " comm.send(m);\n", - " };\n", - " // Register the callback with on_msg.\n", - " comm.on_msg(function (msg) {\n", - " //console.log('receiving', msg['content']['data'], msg);\n", - " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", - " ws.onmessage(msg['content']['data']);\n", - " });\n", - " return ws;\n", - "};\n", - "\n", - "mpl.mpl_figure_comm = function (comm, msg) {\n", - " // This is the function which gets called when the mpl process\n", - " // starts-up an IPython Comm through the \"matplotlib\" channel.\n", - "\n", - " var id = msg.content.data.id;\n", - " // Get hold of the div created by the display call when the Comm\n", - " // socket was opened in Python.\n", - " var element = document.getElementById(id);\n", - " var ws_proxy = comm_websocket_adapter(comm);\n", - "\n", - " function ondownload(figure, _format) {\n", - " window.open(figure.canvas.toDataURL());\n", - " }\n", - "\n", - " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", - "\n", - " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", - " // web socket which is closed, not our websocket->open comm proxy.\n", - " ws_proxy.onopen();\n", - "\n", - " fig.parent_element = element;\n", - " fig.cell_info = mpl.find_output_cell(\"
\");\n", - " if (!fig.cell_info) {\n", - " console.error('Failed to find cell for figure', id, fig);\n", - " return;\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_close = function (fig, msg) {\n", - " var width = fig.canvas.width / mpl.ratio;\n", - " fig.root.removeEventListener('remove', this._remove_fig_handler);\n", - "\n", - " // Update the output cell to use the data from the current canvas.\n", - " fig.push_to_output();\n", - " var dataURL = fig.canvas.toDataURL();\n", - " // Re-enable the keyboard manager in IPython - without this line, in FF,\n", - " // the notebook keyboard shortcuts fail.\n", - " IPython.keyboard_manager.enable();\n", - " fig.parent_element.innerHTML =\n", - " '';\n", - " fig.close_ws(fig, msg);\n", - "};\n", - "\n", - "mpl.figure.prototype.close_ws = function (fig, msg) {\n", - " fig.send_message('closing', msg);\n", - " // fig.ws.close()\n", - "};\n", - "\n", - "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", - " // Turn the data on the canvas into data in the output cell.\n", - " var width = this.canvas.width / mpl.ratio;\n", - " var dataURL = this.canvas.toDataURL();\n", - " this.cell_info[1]['text/html'] =\n", - " '';\n", - "};\n", - "\n", - "mpl.figure.prototype.updated_canvas_event = function () {\n", - " // Tell IPython that the notebook contents must change.\n", - " IPython.notebook.set_dirty(true);\n", - " this.send_message('ack', {});\n", - " var fig = this;\n", - " // Wait a second, then push the new image to the DOM so\n", - " // that it is saved nicely (might be nice to debounce this).\n", - " setTimeout(function () {\n", - " fig.push_to_output();\n", - " }, 1000);\n", - "};\n", - "\n", - "mpl.figure.prototype._init_toolbar = function () {\n", - " var fig = this;\n", - "\n", - " var toolbar = document.createElement('div');\n", - " toolbar.classList = 'btn-toolbar';\n", - " this.root.appendChild(toolbar);\n", - "\n", - " function on_click_closure(name) {\n", - " return function (_event) {\n", - " return fig.toolbar_button_onclick(name);\n", - " };\n", - " }\n", - "\n", - " function on_mouseover_closure(tooltip) {\n", - " return function (event) {\n", - " if (!event.currentTarget.disabled) {\n", - " return fig.toolbar_button_onmouseover(tooltip);\n", - " }\n", - " };\n", - " }\n", - "\n", - " fig.buttons = {};\n", - " var buttonGroup = document.createElement('div');\n", - " buttonGroup.classList = 'btn-group';\n", - " var button;\n", - " for (var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " /* Instead of a spacer, we start a new button group. */\n", - " if (buttonGroup.hasChildNodes()) {\n", - " toolbar.appendChild(buttonGroup);\n", - " }\n", - " buttonGroup = document.createElement('div');\n", - " buttonGroup.classList = 'btn-group';\n", - " continue;\n", - " }\n", - "\n", - " button = fig.buttons[name] = document.createElement('button');\n", - " button.classList = 'btn btn-default';\n", - " button.href = '#';\n", - " button.title = name;\n", - " button.innerHTML = '';\n", - " button.addEventListener('click', on_click_closure(method_name));\n", - " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", - " buttonGroup.appendChild(button);\n", - " }\n", - "\n", - " if (buttonGroup.hasChildNodes()) {\n", - " toolbar.appendChild(buttonGroup);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = document.createElement('span');\n", - " status_bar.classList = 'mpl-message pull-right';\n", - " toolbar.appendChild(status_bar);\n", - " this.message = status_bar;\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = document.createElement('div');\n", - " buttongrp.classList = 'btn-group inline pull-right';\n", - " button = document.createElement('button');\n", - " button.classList = 'btn btn-mini btn-primary';\n", - " button.href = '#';\n", - " button.title = 'Stop Interaction';\n", - " button.innerHTML = '';\n", - " button.addEventListener('click', function (_evt) {\n", - " fig.handle_close(fig, {});\n", - " });\n", - " button.addEventListener(\n", - " 'mouseover',\n", - " on_mouseover_closure('Stop Interaction')\n", - " );\n", - " buttongrp.appendChild(button);\n", - " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", - " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", - "};\n", - "\n", - "mpl.figure.prototype._remove_fig_handler = function () {\n", - " this.close_ws(this, {});\n", - "};\n", - "\n", - "mpl.figure.prototype._root_extra_style = function (el) {\n", - " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", - " el.addEventListener('remove', this._remove_fig_handler);\n", - "};\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function (el) {\n", - " // this is important to make the div 'focusable\n", - " el.setAttribute('tabindex', 0);\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " } else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype._key_event_extra = function (event, _name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager) {\n", - " manager = IPython.keyboard_manager;\n", - " }\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which === 13) {\n", - " this.canvas_div.blur();\n", - " // select the cell after this one\n", - " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", - " IPython.notebook.select(index + 1);\n", - " }\n", - "};\n", - "\n", - "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", - " fig.ondownload(fig, null);\n", - "};\n", - "\n", - "mpl.find_output_cell = function (html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i = 0; i < ncells; i++) {\n", - " var cell = cells[i];\n", - " if (cell.cell_type === 'code') {\n", - " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", - " var data = cell.output_area.outputs[j];\n", - " if (data.data) {\n", - " // IPython >= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] === html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "};\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel !== null) {\n", - " IPython.notebook.kernel.comm_manager.register_target(\n", - " 'matplotlib',\n", - " mpl.mpl_figure_comm\n", - " );\n", - "}\n" - ], "text/plain": [ - "" - ] + "HBox(children=(FloatProgress(value=0.0, max=10.0), HTML(value='')))" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "50963f48cbd64d77b0b7ceee27f27adc" + } }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} }, { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "output_type": "stream", + "name": "stdout", + "text": [ + "\n" + ] } ], + "metadata": { + "tags": [] + } + }, + { + "cell_type": "markdown", + "source": [ + "The simulation generates a log of its comuptation: this log can be retrieved by using ```simulator.get_log``` - and written to a file for latter processing by the engine with ```simulator.engine.write_log```." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, "source": [ "# Get dictionary of logged scalar variables\n", "log_data = simulator.log_data\n", "\n", "# Let's plot the joint position to see the pendulum fall.\n", - "%matplotlib notebook\n", + "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", "\n", "plt.plot(log_data['Global.Time'], log_data['HighLevelController.currentPositionPendulum'])\n", "plt.title('Pendulum angle (rad)')\n", "plt.grid()\n", "plt.show()" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "The results of a simulation can also be visualized in a 3D viewer: either `gepetto-gui` or `meshcat`. We use the latter here as it can be integrated in jupyter." - ] + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": false - }, + "execution_count": 3, + "source": [ + "camera_xyzrpy = ([5.0, 0.0, 2.0e-5], [np.pi/2, 0.0, np.pi/2])\n", + "simulator.replay(camera_xyzrpy=camera_xyzrpy)" + ], "outputs": [ { + "output_type": "display_data", "data": { + "text/plain": [ + "" + ], "text/html": [ "\n", - "
\n", - " \n", + "\" style=\"\n", + " width: 100%; height: 100%; border: none;\">\n", + " \n", "
\n", " " - ], - "text/plain": [ - "" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "camera_xyzrpy = ([5.0, 0.0, 2.0e-5], [np.pi/2, 0.0, np.pi/2])\n", - "simulator.replay(camera_xyzrpy=camera_xyzrpy)" - ] + "metadata": { + "scrolled": false + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Let's now add a controller: a simple PD to hold the pendulum straight." - ] + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ee9647b3373f4ae7ba49b7cf09e53f45", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(FloatProgress(value=0.0, max=10.0), HTML(value='')))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": 4, "source": [ "Kp = 5000\n", "Kd = 0.05\n", @@ -2706,38 +1951,34 @@ "\n", "# Update the simulator to use the new controller\n", "simulator.set_controller(controller)" - ] + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "jiminy_py.core.hresult_t.SUCCESS" + ] + }, + "metadata": {}, + "execution_count": 4 + } + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Adding external forces.\n", "\n", "External forces can be applied to the system through a force profile : a function outputing a force on\n", "a specific body as as a function of time dans state." - ] + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "250013949ba94016bf2620029d3e9a0f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(FloatProgress(value=0.0, max=10.0), HTML(value='')))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": 5, "source": [ "# Apply a force of 500N in the Y direction between t = 2.5 and t = 3s\n", "def force_profile(t, q, v, f):\n", @@ -2752,14 +1993,37 @@ "\n", "# Replay the simulation with new controller and external forces\n", "simulator.replay(camera_xyzrpy=camera_xyzrpy)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "HBox(children=(FloatProgress(value=0.0, max=10.0), HTML(value='')))" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "0b45b87c06234cb69683051d7f105407" + } + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n" + ] + } + ], + "metadata": {} } ], "metadata": { "kernelspec": { - "display_name": "Python 3.6.9 64-bit", - "language": "python", - "name": "python36964bitab75b9a2d67240bd8443b741e8c3d97d" + "name": "python3", + "display_name": "Python 3.6.9 64-bit" }, "language_info": { "codemirror_mode": { @@ -2772,6 +2036,9 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.9" + }, + "interpreter": { + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" } }, "nbformat": 4, diff --git a/python/gym_jiminy/common/gym_jiminy/common/envs/env_generic.py b/python/gym_jiminy/common/gym_jiminy/common/envs/env_generic.py index 093e7a1ec..e85f073ad 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/envs/env_generic.py +++ b/python/gym_jiminy/common/gym_jiminy/common/envs/env_generic.py @@ -784,11 +784,17 @@ def plot(self, **kwargs: Any) -> None: # Call base implementation self.simulator.plot(**kwargs) + # Extract log data + log_data = self.simulator.log_data + if not log_data: + raise RuntimeError( + "Nothing to plot. Please run a simulation before calling " + "`plot` method.") + # Extract action. # If telemetry action fieldnames is a dictionary, it cannot be nested. # In such a case, keys corresponds to subplots, and values are # individual scalar data over time to be displayed to the same subplot. - log_data = self.simulator.log_data t = log_data["Global.Time"] tab_data = {} if self.logfile_action_headers is None: @@ -863,6 +869,7 @@ def replay(self, enable_travelling: bool = True, **kwargs: Any) -> None: def play_interactive(env: Union["BaseJiminyEnv", gym.Wrapper], enable_travelling: Optional[bool] = None, start_paused: bool = True, + enable_is_done: bool = True, verbose: bool = True, **kwargs: Any) -> None: """Activate interact mode enabling to control the robot using keyboard. @@ -900,8 +907,10 @@ def play_interactive(env: Union["BaseJiminyEnv", gym.Wrapper], assert isinstance(self, BaseJiminyEnv), ( "Unwrapped environment must derived from `BaseJiminyEnv`.") - # Enable play interactive mode flag + # Enable play interactive flag and make sure training flag is disabled + is_training = self.is_training self._is_interactive = True + self.is_training = False # Make sure viewer gui is open, so that the viewer will shared external # forces with the robot automatically. @@ -927,11 +936,13 @@ def play_interactive(env: Union["BaseJiminyEnv", gym.Wrapper], # Define interactive loop def _interact(key: Optional[str] = None) -> bool: - nonlocal obs, reward + nonlocal obs, reward, enable_is_done action = self._key_to_action( key, obs, reward, **{"verbose": verbose, **kwargs}) obs, reward, done, _ = env.step(action) env.render() + if not enable_is_done and env.robot.has_freeflyer: + return env.system_state.q[2] < 0.0 return done # Run interactive loop @@ -948,8 +959,9 @@ def _interact(key: Optional[str] = None) -> bool: if self.simulator.is_simulation_running: self.simulator.stop() - # Disable play interactive mode flag + # Disable play interactive mode flag and restore training flag self._is_interactive = False + self.is_training = is_training def train(self) -> None: """Sets the environment in training mode. @@ -1098,6 +1110,10 @@ def refresh_observation(self) -> None: # type: ignore[override] .. note:: This method is called and the end of every low-level `Engine.step`. + .. note:: + Note that `np.nan` values will be automatically clipped to 0.0 by + `get_observation` method before return it, so it is valid. + .. warning:: In practice, it updates the internal buffer directly for the sake of efficiency. diff --git a/python/gym_jiminy/common/gym_jiminy/common/envs/env_locomotion.py b/python/gym_jiminy/common/gym_jiminy/common/envs/env_locomotion.py index a6ced6910..a61822642 100644 --- a/python/gym_jiminy/common/gym_jiminy/common/envs/env_locomotion.py +++ b/python/gym_jiminy/common/gym_jiminy/common/envs/env_locomotion.py @@ -391,7 +391,7 @@ def compute_reward(self, # type: ignore[override] if 'energy' in reward_mixture_keys: v_mot = self.robot.sensors_data[encoder.type][1] command = self.system_state.command - power_consumption = sum(np.maximum(command * v_mot, 0.0)) + power_consumption = np.sum(np.maximum(command * v_mot, 0.0)) power_consumption_rel = \ power_consumption / self._power_consumption_max reward_dict['energy'] = - power_consumption_rel diff --git a/python/gym_jiminy/common/setup.py b/python/gym_jiminy/common/setup.py index 3355eb5a3..32410d91e 100644 --- a/python/gym_jiminy/common/setup.py +++ b/python/gym_jiminy/common/setup.py @@ -55,7 +55,7 @@ # Disable automatic forward compatibility with newer versions because # numba relies on llvmlite, for which wheels take some time before # being available on Pypi, making the whole installation process fail. - # Version >=0.53 is required to support Python 3.9. + # >=0.53 is required to support Python 3.9. "numba==0.53.1", # Standard interface library for reinforcement learning. # - 0.17.3 introduces iterable space dict diff --git a/python/gym_jiminy/rllib/gym_jiminy/rllib/ppo.py b/python/gym_jiminy/rllib/gym_jiminy/rllib/ppo.py index 923f3e361..daa14dd61 100644 --- a/python/gym_jiminy/rllib/gym_jiminy/rllib/ppo.py +++ b/python/gym_jiminy/rllib/gym_jiminy/rllib/ppo.py @@ -11,8 +11,9 @@ from ray.rllib.policy.policy import Policy from ray.rllib.policy.sample_batch import SampleBatch from ray.rllib.policy.view_requirement import ViewRequirement -from ray.rllib.utils.typing import TensorType, TrainerConfigDict from ray.rllib.utils.torch_ops import l2_loss +from ray.rllib.utils.typing import TensorType, TrainerConfigDict + from ray.rllib.agents.ppo import DEFAULT_CONFIG, PPOTrainer from ray.rllib.agents.ppo.ppo_torch_policy import ( ppo_surrogate_loss, kl_and_loss_stats, setup_mixins, PPOTorchPolicy) @@ -21,6 +22,7 @@ DEFAULT_CONFIG = PPOTrainer.merge_trainer_configs( DEFAULT_CONFIG, { + "noise_scale": 1.0, "symmetric_policy_reg": 0.0, "caps_temporal_reg": 0.0, "caps_spatial_reg": 0.0, @@ -57,18 +59,30 @@ def ppo_init(policy: Policy, policy._mean_global_caps_loss = 0.0 policy._l2_reg_loss = 0.0 - # Convert to torch.Tensor observation bounds - policy._observation_space_low = \ - torch.from_numpy(policy.observation_space.low).to(dtype=torch.float32) - policy._observation_space_high = \ - torch.from_numpy(policy.observation_space.high).to(dtype=torch.float32) + # Check if the policy has observation filter. If so, disable element-wise + # observation sensitivity. + obs_filter = policy.config["observation_filter"] + if obs_filter == "NoFilter": + policy._is_obs_normalized = False + elif obs_filter == "MeanStdFilter": + policy._is_obs_normalized = True + else: + raise NotImplementedError( + "Only 'NoFilter' and 'MeanStdFilter' are supported.") + + # Extract original observation space + try: + observation_space = policy.observation_space.original_space + except AttributeError as e: + raise NotImplementedError( + "Only 'Dict' original observation space is supported.") from e - # Convert to torch.Tensor observation sensitivity data - observation_space = policy.observation_space.original_space - for field, scale in observation_space.sensitivity.items(): - if not isinstance(scale, torch.Tensor): - scale = torch.from_numpy(scale).to(dtype=torch.float32) - observation_space.sensitivity[field] = scale + # Convert to torch.Tensor observation sensitivity data if necessary + if not policy._is_obs_normalized: + for field, scale in observation_space.sensitivity.items(): + if not isinstance(scale, torch.Tensor): + scale = torch.from_numpy(scale).to(dtype=torch.float32) + observation_space.sensitivity[field] = scale # Transpose and convert to torch.Tensor the observation mirroring data for field, mirror_mat in observation_space.mirror_mat.items(): @@ -113,29 +127,28 @@ def ppo_loss(policy: Policy, # Append the training batches to the set train_batches["prev"] = train_batch_copy - if policy.config["caps_spatial_reg"] > 0.0 or \ - policy.config["caps_global_reg"] > 0.0: + if policy._spatial_reg > 0.0 or policy.config["caps_global_reg"] > 0.0: # Shallow copy the original training batch train_batch_copy = train_batch.copy(shallow=True) # Generate noisy observation based on specified sensivity - offset = 0 - observation_noisy = observation_true.clone() - batch_dim = observation_true.shape[:-1] - observation_space = policy.observation_space.original_space - for field, scale in observation_space.sensitivity.items(): - scale = scale.to(device) - observation_space.sensitivity[field] = scale - unit_noise = torch.randn((*batch_dim, len(scale)), device=device) - slice_idx = slice(offset, offset + len(scale)) - observation_noisy[..., slice_idx].addcmul_(scale, unit_noise) - offset += len(scale) - torch.min(torch.max( - observation_noisy, - policy._observation_space_low.to(device), - out=observation_noisy), - policy._observation_space_high.to(device), - out=observation_noisy) + if policy._is_obs_normalized: + observation_noisy = torch.normal( + observation_true, policy.config["noise_scale"]) + else: + offset = 0 + observation_noisy = observation_true.clone() + batch_dim = observation_true.shape[:-1] + observation_space = policy.observation_space.original_space + for field, scale in observation_space.sensitivity.items(): + scale = scale.to(device) + observation_space.sensitivity[field] = scale + unit_noise = torch.randn( + (*batch_dim, len(scale)), device=device) + slice_idx = slice(offset, offset + len(scale)) + observation_noisy[..., slice_idx].addcmul_( + policy.config["noise_scale"] * scale, unit_noise) + offset += len(scale) # Replace current observation by the noisy one train_batch_copy["obs"] = observation_noisy @@ -225,8 +238,7 @@ def value_function(self, *args: Any, **kwargs: Any) -> torch.Tensor: action_mean_prev = action_dist_prev.deterministic_sample() # Compute the mean action corresponding to the noisy observation - if policy.config["caps_spatial_reg"] > 0.0 or \ - policy.config["caps_global_reg"] > 0.0: + if policy._spatial_reg > 0.0 or policy.config["caps_global_reg"] > 0.0: action_logits_noisy = logits["noisy"] if issubclass(dist_class, TorchDiagGaussian): action_mean_noisy, _ = torch.chunk(action_logits_noisy, 2, dim=1) @@ -249,21 +261,23 @@ def value_function(self, *args: Any, **kwargs: Any) -> torch.Tensor: if policy.config["caps_temporal_reg"] > 0.0: # Minimize the difference between the successive action mean policy._mean_temporal_caps_loss = torch.mean( - (action_mean_prev - action_mean_true) ** 2) + (action_mean_prev - action_mean_true).abs()) # Add temporal smoothness loss to total loss total_loss += policy.config["caps_temporal_reg"] * \ policy._mean_temporal_caps_loss - if policy.config["caps_spatial_reg"] > 0.0: + if policy._spatial_reg > 0.0: # Minimize the difference between the original action mean and the # one corresponding to the noisy observation. policy._mean_spatial_caps_loss = torch.mean( - (action_mean_noisy - action_mean_true) ** 2) + torch.sum(( + action_mean_noisy - action_mean_true) ** 2, dim=-1) / + torch.sum(( + observation_noisy - observation_true) ** 2, dim=-1)) # Add spatial smoothness loss to total loss - total_loss += policy.config["caps_spatial_reg"] * \ - policy._mean_spatial_caps_loss + total_loss += policy._spatial_reg * policy._mean_spatial_caps_loss if policy.config["caps_global_reg"] > 0.0: # Minimize the magnitude of action mean @@ -285,8 +299,8 @@ def value_function(self, *args: Any, **kwargs: Any) -> torch.Tensor: if policy.config["l2_reg"] > 0.0: # Add actor l2-regularization loss l2_reg_loss = 0.0 - for name, params in model.state_dict().items(): - if "bias" not in name: + for name, params in model.named_parameters(): + if not name.endswith("bias"): l2_reg_loss += l2_loss(params) policy._l2_reg_loss = l2_reg_loss @@ -308,7 +322,7 @@ def ppo_stats(policy: Policy, stats_dict["symmetry"] = policy._mean_symmetric_policy_loss if policy.config["caps_temporal_reg"] > 0.0: stats_dict["temporal_smoothness"] = policy._mean_temporal_caps_loss - if policy.config["caps_spatial_reg"] > 0.0: + if policy._spatial_reg > 0.0: stats_dict["spatial_smoothness"] = policy._mean_spatial_caps_loss if policy.config["caps_global_reg"] > 0.0: stats_dict["global_smoothness"] = policy._mean_global_caps_loss @@ -322,7 +336,7 @@ def ppo_stats(policy: Policy, before_loss_init=ppo_init, loss_fn=ppo_loss, stats_fn=ppo_stats, - get_default_config=lambda: DEFAULT_CONFIG, + get_default_config=lambda: DEFAULT_CONFIG ) @@ -340,6 +354,7 @@ def get_policy_class( get_policy_class=get_policy_class ) + __all__ = [ "DEFAULT_CONFIG", "PPOTorchPolicy", diff --git a/python/gym_jiminy/rllib/gym_jiminy/rllib/utilities.py b/python/gym_jiminy/rllib/gym_jiminy/rllib/utilities.py index 455a6b1e1..fabf8bdd2 100644 --- a/python/gym_jiminy/rllib/gym_jiminy/rllib/utilities.py +++ b/python/gym_jiminy/rllib/gym_jiminy/rllib/utilities.py @@ -309,7 +309,7 @@ def build_policy_wrapper(policy: Policy, action_space = policy.action_space # Build preprocessor to flatten environment observation - observation_space_orig = policy.observation_space + observation_space_orig = observation_space if hasattr(observation_space_orig, "original_space"): observation_space_orig = observation_space.original_space preprocessor_class = get_preprocessor(observation_space_orig) @@ -552,16 +552,19 @@ def train(train_agent: Trainer, # Ascii histogram if requested if verbose: - plt.clp() - plt.subplots(1, 2) - for i, (title, data) in enumerate(zip( - ("Episode duration", "Total reward"), - (duration, total_rewards))): - plt.subplot(1, i) - plt.hist(data, HISTOGRAM_BINS) - plt.plotsize(50, 20) - plt.title(title) - plt.show() + try: + plt.clp() + plt.subplots(1, 2) + for i, (title, data) in enumerate(zip( + ("Episode duration", "Total reward"), + (duration, total_rewards))): + plt.subplot(1, i) + plt.hist(data, HISTOGRAM_BINS) + plt.plotsize(50, 20) + plt.title(title) + plt.show() + except IndexError as e: + logger.warning(f"Rendering statistics failed: {e}") # Backup the policy if checkpoint_period > 0 and iter_num % checkpoint_period == 0: diff --git a/python/gym_jiminy/rllib/setup.py b/python/gym_jiminy/rllib/setup.py index 191f560c8..3e773ab37 100644 --- a/python/gym_jiminy/rllib/setup.py +++ b/python/gym_jiminy/rllib/setup.py @@ -29,7 +29,7 @@ packages=find_namespace_packages(), install_requires=[ f"gym_jiminy~={version}", - "ray[default,rllib]<=1.5.1", + "ray[default,rllib]>=1.4.0", "plotext" ], zip_safe=False diff --git a/python/gym_jiminy/unit_py/data/atlas_standing_meshcat_1.png b/python/gym_jiminy/unit_py/data/atlas_standing_meshcat_1.png new file mode 100644 index 000000000..666a0ce35 Binary files /dev/null and b/python/gym_jiminy/unit_py/data/atlas_standing_meshcat_1.png differ diff --git a/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_3.png b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_3.png new file mode 100644 index 000000000..3d21a3c6e Binary files /dev/null and b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_3.png differ diff --git a/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_4.png b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_4.png new file mode 100644 index 000000000..eb7ed383f Binary files /dev/null and b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_4.png differ diff --git a/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_5.png b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_5.png new file mode 100644 index 000000000..5cff8b071 Binary files /dev/null and b/python/gym_jiminy/unit_py/data/cassie_standing_meshcat_5.png differ diff --git a/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_6.png b/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_6.png new file mode 100644 index 000000000..9b0fea73d Binary files /dev/null and b/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_6.png differ diff --git a/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_7.png b/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_7.png new file mode 100644 index 000000000..cfeef3d9a Binary files /dev/null and b/python/gym_jiminy/unit_py/data/cassie_standing_panda3d_7.png differ diff --git a/python/gym_jiminy/unit_py/test_pipeline_control.py b/python/gym_jiminy/unit_py/test_pipeline_control.py index f19ccce45..a7850016c 100644 --- a/python/gym_jiminy/unit_py/test_pipeline_control.py +++ b/python/gym_jiminy/unit_py/test_pipeline_control.py @@ -14,8 +14,10 @@ from jiminy_py.core import EncoderSensor as encoder from jiminy_py.viewer import Viewer -from gym_jiminy.envs import AtlasPDControlJiminyEnv -from gym_jiminy.envs import CassiePDControlJiminyEnv +from gym_jiminy.envs import AtlasPDControlJiminyEnv, CassiePDControlJiminyEnv + + +IMAGE_DIFF_THRESHOLD = 0.3 class PipelineControl(unittest.TestCase): @@ -60,16 +62,16 @@ def _test_pid_standing(self): rgb_array_abs_orig = ( rgba_array_rel_orig[..., :3] * 255).astype(np.uint8) img_diff = np.mean(np.abs(rgb_array - rgb_array_abs_orig)) - if img_diff < 0.1: + if img_diff < IMAGE_DIFF_THRESHOLD: break - if img_diff > 0.1: + if img_diff > IMAGE_DIFF_THRESHOLD: img_obj = Image.fromarray(rgb_array) raw_bytes = io.BytesIO() img_obj.save(raw_bytes, "PNG") raw_bytes.seek(0) print(f"{self.env.robot.name} - {self.env.viewer.backend}:", base64.b64encode(raw_bytes.read())) - self.assertTrue(img_diff < 0.1) + self.assertTrue(img_diff < IMAGE_DIFF_THRESHOLD) # Get the simulation log log_data = self.env.log_data diff --git a/python/jiminy_py/setup.py b/python/jiminy_py/setup.py index c92faaeb5..eb22af578 100644 --- a/python/jiminy_py/setup.py +++ b/python/jiminy_py/setup.py @@ -107,9 +107,12 @@ def finalize_options(self) -> None: # Parser for Jiminy's hardware description file. "toml", # Web-based mesh visualizer used as Viewer's backend. - # 0.19.0 introduces many new features, including loading generic + # 0.0.19 introduces many new features, including loading generic # geometries and jiminy_py viewer releases on it for rendering # collision bodies. + # 0.3.1 updates threejs from 122 to 132, breakin compatibility with the + # old, now deprecated, geometry class used to internally to display + # tile floor. "meshcat>=0.0.19", # Standalone mesh visualizer used as Viewer's backend. # Panda3d>1.10.9 adds support of Nvidia EGL rendering without X11 diff --git a/python/jiminy_py/src/jiminy_py/dynamics.py b/python/jiminy_py/src/jiminy_py/dynamics.py index 07b4c1dc5..967b67dbc 100644 --- a/python/jiminy_py/src/jiminy_py/dynamics.py +++ b/python/jiminy_py/src/jiminy_py/dynamics.py @@ -70,8 +70,8 @@ def velocityXYZQuatToXYZRPY(xyzquat: np.ndarray, However, it is not the case for the linear velocity. .. warning:: - Linear velocity in XYZRPY must be local-world-aligned frame, while - returned linear velocity in XYZQuat is in local frame. + Linear velocity in XYZQuat must be local frame, while returned linear + velocity in XYZRPY is in local-world-aligned frame. """ quat = pin.Quaternion(xyzquat[3:]) rpy = matrixToRpy(quat.matrix()) diff --git a/python/jiminy_py/src/jiminy_py/plot.py b/python/jiminy_py/src/jiminy_py/plot.py index 8c4cd0a9b..fbac743b6 100644 --- a/python/jiminy_py/src/jiminy_py/plot.py +++ b/python/jiminy_py/src/jiminy_py/plot.py @@ -298,6 +298,7 @@ def add_tab(self, plot_method(ax, time, data) if self.tabs_data: self.figure.delaxes(ax) + ax.grid() axes = [ax] # Get unique legend for every subplots diff --git a/python/jiminy_py/src/jiminy_py/viewer/meshcat/index.html b/python/jiminy_py/src/jiminy_py/viewer/meshcat/index.html index eaa05cefe..33d7279ac 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/meshcat/index.html +++ b/python/jiminy_py/src/jiminy_py/viewer/meshcat/index.html @@ -63,7 +63,8 @@ }); } else { - viewer.connect(); + var ws_url = undefined; + viewer.connect(ws_url); } } catch (e) { console.info("Not connected to MeshCat server: ", e); @@ -72,20 +73,25 @@ // Replace the mesh grid by a filled checkerboard, similar to // the one of Gepetto-gui. The paving size is 1m by 1m. var segments = 20; - var geometry = new MeshCat.THREE.PlaneGeometry(20, 20, segments, segments); - var materialEven = new MeshCat.THREE.MeshBasicMaterial( - {color: 0x222233, side: MeshCat.THREE.DoubleSide}); - var materialOdd = new MeshCat.THREE.MeshBasicMaterial( - {color: 0xf2f2fe, side: MeshCat.THREE.DoubleSide}); - var materials = [materialEven, materialOdd] - for (x of [...Array(segments).keys()]) { - for (y of [...Array(segments).keys()]) { - i = x * segments + y; - j = 2 * i; - geometry.faces[j].materialIndex = geometry.faces[j + 1].materialIndex = (x + y) % 2; + var cmap = [new MeshCat.THREE.Color(0x222233), new MeshCat.THREE.Color(0xf2f2fe)]; + var geometry = new MeshCat.THREE.PlaneBufferGeometry( + segments, segments, segments, segments).toNonIndexed(); + var material = new MeshCat.THREE.MeshBasicMaterial( + {vertexColors: true, side: MeshCat.THREE.DoubleSide}); + var colors = []; + for (var x of [...Array(segments).keys()]) { + for (var y of [...Array(segments).keys()]) { + var color = cmap[(x + y) % 2]; + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); } } - var checkerboard = new MeshCat.THREE.Mesh(geometry, materials); + geometry.setAttribute('color', new MeshCat.THREE.Float32BufferAttribute(colors, 3)); + var checkerboard = new MeshCat.THREE.Mesh(geometry, material); viewer.scene_tree.find(["Grid"]).set_object(checkerboard) viewer.scene_tree.find(["Axes", ""]).object.material.linewidth = 2.5 diff --git a/python/jiminy_py/src/jiminy_py/viewer/meshcat/recorder.py b/python/jiminy_py/src/jiminy_py/viewer/meshcat/recorder.py index 5a948b851..6e1a8c8cc 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/meshcat/recorder.py +++ b/python/jiminy_py/src/jiminy_py/viewer/meshcat/recorder.py @@ -68,7 +68,6 @@ async def launch(self) -> Browser: cmd = self.cmd + [ "--enable-webgl", "--use-gl=egl", - "--disable-frame-rate-limit", "--disable-gpu-vsync", "--ignore-gpu-blacklist", "--ignore-certificate-errors", @@ -77,9 +76,10 @@ async def launch(self) -> Browser: "--disable-setuid-sandbox", "--proxy-server='direct://'", "--proxy-bypass-list=*"] + if "--disable-gpu" in cmd: + cmd.remove("--disable-gpu") if not self.dumpio: - options['stdout'] = subprocess.DEVNULL options['stderr'] = subprocess.DEVNULL if sys.platform.startswith('win'): startupflags = subprocess.CREATE_NEW_PROCESS_GROUP @@ -331,7 +331,7 @@ def release(self) -> None: def _send_request(self, request: str, message: Optional[str] = None, - timeout: float = 15.0) -> None: + timeout: float = 20.0) -> None: if not self.is_open: raise RuntimeError( "Meshcat recorder is not open. Impossible to send requests.") diff --git a/python/jiminy_py/src/jiminy_py/viewer/meshcat/server.py b/python/jiminy_py/src/jiminy_py/viewer/meshcat/server.py index c058bdb5d..88209d009 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/meshcat/server.py +++ b/python/jiminy_py/src/jiminy_py/viewer/meshcat/server.py @@ -73,12 +73,13 @@ def handle_web(self, message: str) -> None: self.bridge.websocket_msg.append(message) if len(self.bridge.websocket_msg) == len(self.bridge.websocket_pool) and \ len(self.bridge.comm_msg) == len(self.bridge.comm_pool): + self.is_waiting_ready_msg = False gathered_msg = ",".join( - self.bridge.websocket_msg + self.bridge.comm_msg) + self.bridge.websocket_msg + list(self.bridge.comm_msg.values())) self.bridge.zmq_socket.send(gathered_msg.encode("utf-8")) self.bridge.zmq_stream.flush() - self.bridge.websocket_msg, self.bridge.comm_msg = [], [] - self.is_waiting_ready_msg = False + self.bridge.comm_msg = {} + self.bridge.websocket_msg = [] WebSocketHandler.on_message = handle_web # noqa @@ -103,7 +104,7 @@ def f(port): # Extra buffers for: comm ids and messages self.comm_pool = set() - self.comm_msg = [] + self.comm_msg = {} self.websocket_msg = [] self.is_waiting_ready_msg = False @@ -133,21 +134,22 @@ def wait_for_websockets(self) -> None: def handle_zmq(self, frames: Sequence[bytes]) -> None: cmd = frames[0].decode("utf-8") if cmd == "ready": + self.comm_stream.flush() if not self.websocket_pool and not self.comm_pool: self.zmq_socket.send(b"") msg = umsgpack.packb({"type": "ready"}) - self.is_waiting_ready_msg = True for websocket in self.websocket_pool: websocket.write_message(msg, binary=True) for comm_id in self.comm_pool: self.forward_to_comm(comm_id, msg) + self.is_waiting_ready_msg = True else: super().handle_zmq(frames) def handle_comm(self, frames: Sequence[bytes]) -> None: cmd = frames[0].decode("utf-8") + comm_id = f"{cmd.split(':', 1)[1]}".encode() if cmd.startswith("open:"): - comm_id = f"{cmd.split(':', 1)[1]}".encode() self.send_scene(comm_id=comm_id) self.comm_pool.add(comm_id) if self.is_waiting_ready_msg: @@ -157,19 +159,21 @@ def handle_comm(self, frames: Sequence[bytes]) -> None: # Using `discard` over `remove` to avoid raising exception if # 'comm_id' is not found. It may happend if an old comm is closed # after Jupyter-notebook reset for instance. - comm_id = f"{cmd.split(':', 1)[1]}".encode() self.comm_pool.discard(comm_id) + self.comm_msg.pop(comm_id, None) elif cmd.startswith("data:"): message = f"{cmd.split(':', 2)[2]}" - self.comm_msg.append(message) - if (len(self.websocket_msg) == len(self.websocket_pool) and - len(self.comm_msg) == len(self.comm_pool)): - gathered_msg = ",".join( - self.websocket_msg + self.comm_msg) - self.zmq_socket.send(gathered_msg.encode("utf-8")) - self.zmq_stream.flush() - self.websocket_msg, self.comm_msg = [], [] - self.is_waiting_ready_msg = False + self.comm_msg[comm_id] = message + if self.is_waiting_ready_msg and \ + len(self.websocket_msg) == len(self.websocket_pool) and \ + len(self.comm_msg) == len(self.comm_pool): + self.is_waiting_ready_msg = False + gathered_msg = ",".join( + self.websocket_msg + list(self.comm_msg.values())) + self.zmq_socket.send(gathered_msg.encode("utf-8")) + self.zmq_stream.flush() + self.comm_msg = {} + self.websocket_msg = [] def forward_to_websockets(self, frames: Sequence[bytes]) -> None: super().forward_to_websockets(frames) diff --git a/python/jiminy_py/src/jiminy_py/viewer/meshcat/wrapper.py b/python/jiminy_py/src/jiminy_py/viewer/meshcat/wrapper.py index 130ad2961..1289f316c 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/meshcat/wrapper.py +++ b/python/jiminy_py/src/jiminy_py/viewer/meshcat/wrapper.py @@ -21,15 +21,6 @@ from .recorder import MeshcatRecorder -if interactive_mode() == 1: - # The IO message rate limit has already been increased to 1e6 on Google - # Colab, so no need to throw this warning. - logging.warning( - "You may experience some lags while replaying a simulation.\n" - "Consider increasing the IO message rate limit by adding the " - "extra argument '--NotebookApp.iopub_msg_rate_limit=100000' when " - "executing 'jupyter notebook'.") - if interactive_mode(): # Google colab is using an older version of ipykernel (4.10), which is # not compatible with >= 5.0. The new API is more flexible and enable @@ -130,19 +121,19 @@ def __call__(self, unsafe: bool = False) -> None: msg['header']['msg_type'].startswith('comm_'): # Comm message. Analyzing message content to determine if # it is related to meshcat or not. - if not msg['header']['msg_type'] == 'comm_close': - content = self.__kernel.session.unpack(msg['content']) - data = content['data'] - else: + if msg['header']['msg_type'] == 'comm_close': # All comm_close messages are processed because Google # Colab API does not support sending data on close. - data = "'meshcat:close" + data = "meshcat:close" + else: + content = self.__kernel.session.unpack(msg['content']) + data = content.get('data', '') if isinstance(data, str) and data.startswith('meshcat:'): # Comm message related to meshcat. Processing it right # now and moving to the next message without puting it # back into the queue. tornado.gen.maybe_future(dispatch(*args)) - continue + continue # The message is not related to meshcat comm, so putting it # back in the queue after lowering its priority so that it is @@ -153,11 +144,11 @@ def __call__(self, unsafe: bool = False) -> None: # SHELL_PRIORITY by default. self.__kernel.msg_queue.put_nowait( (SHELL_PRIORITY + 1, t, dispatch, args)) - - # Ensure the eventloop wakes up - self.__kernel.io_loop.add_callback(lambda: None) self.qsize_old = self.__kernel.msg_queue.qsize() + # Ensure the eventloop wakes up + self.__kernel.io_loop.add_callback(lambda: None) + process_kernel_comm = CommProcessor() # Monkey-patch meshcat ViewerWindow 'send' method to process queued comm @@ -181,6 +172,10 @@ def __init__(self, comm_url: str): self.n_comm = 0 self.n_message = 0 + self.__ioloop = None + self.__comm_socket = None + self.__comm_stream = None + def forward_comm_thread(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -191,6 +186,10 @@ def forward_comm_thread(): self.__comm_stream = ZMQStream(self.__comm_socket, self.__ioloop) self.__comm_stream.on_recv(self.__forward_to_ipython) self.__ioloop.start() + self.__ioloop.close() + self.__ioloop = None + self.__comm_socket = None + self.__comm_stream = None self.__thread = threading.Thread(target=forward_comm_thread) self.__thread.daemon = True @@ -208,9 +207,11 @@ def close(self) -> None: self.n_message = 0 self.__kernel.comm_manager.unregister_target( 'meshcat', self.__comm_register) - self.__thread._stop() self.__comm_stream.close(linger=5) self.__comm_socket.close(linger=5) + self.__ioloop.add_callback(lambda: self.__ioloop.stop()) + self.__thread.join() + self.__thread = None def __forward_to_ipython(self, frames: Sequence[bytes]) -> None: comm_id, cmd = frames # There must be always two parts each messages @@ -438,7 +439,9 @@ def add_frame(self) -> None: def capture_frame(self, width: Optional[int] = None, height: Optional[int] = None) -> str: - if not self.recorder.is_open: + if self.recorder.is_open: + self.wait(require_client=False) + else: self.recorder.open() self.wait(require_client=True) return self.recorder.capture_frame(width, height) diff --git a/python/jiminy_py/src/jiminy_py/viewer/panda3d/panda3d_visualizer.py b/python/jiminy_py/src/jiminy_py/viewer/panda3d/panda3d_visualizer.py index 35202392f..bdcd8559b 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/panda3d/panda3d_visualizer.py +++ b/python/jiminy_py/src/jiminy_py/viewer/panda3d/panda3d_visualizer.py @@ -4,6 +4,7 @@ import sys import math import array +import pickle import warnings import xml.etree.ElementTree as ET from datetime import datetime @@ -170,7 +171,7 @@ def make_cone(num_sides: int = 16) -> Geom: """Create a close shaped cone, approximate by a pyramid with regular convex n-sided polygon base. - For reference about refular polygon: + For reference about regular polygon: https://en.wikipedia.org/wiki/Regular_polygon """ # Define vertex format @@ -206,8 +207,53 @@ def make_cone(num_sides: int = 16) -> Geom: prim.add_vertices(i, i + 1, num_sides + 1) prim.add_vertices(i + 1, i, num_sides + 2) + # Create geometry object geom = Geom(vdata) geom.add_primitive(prim) + + return geom + + +def make_height_map(height_map: Callable[ + [np.ndarray], Tuple[float, np.ndarray]], + grid_size: float, + grid_unit: float) -> Geom: + """Create height map. + """ + # Compute grid size and number of vertices + grid_dim = int(np.ceil(grid_size / grid_unit)) + 1 + num_vertices = grid_dim ** 2 + + # Define vertex format + vformat = GeomVertexFormat.get_v3n3t2() + vdata = GeomVertexData('vdata', vformat, Geom.UH_static) + vdata.uncleanSetNumRows(num_vertices) + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + tcoord = GeomVertexWriter(vdata, 'texcoord') + + # # Add grid points + for x in np.arange(grid_dim) * grid_unit - grid_size / 2.0: + for y in np.arange(grid_dim) * grid_unit - grid_size / 2.0: + height, normal_i = height_map(np.array([x, y, 0.0])) + vertex.addData3(x, y, height) + normal.addData3(*normal_i) + tcoord.addData2(x, y) + + # Make triangles + prim = GeomTriangles(Geom.UH_static) + for j in range(grid_dim): + for i in range(grid_dim - 1): + k = j * grid_dim + i + if j < grid_dim - 1: + prim.add_vertices(k + 1, k, k + grid_dim) + if j > 0: + prim.add_vertices(k, k + 1, k + 1 - grid_dim) + + # Create geometry object + geom = Geom(vdata) + geom.add_primitive(prim) + return geom @@ -687,22 +733,48 @@ def _make_axes(self) -> NodePath: node.set_scale(0.3) return node - def _make_floor(self) -> NodePath: + def _make_floor(self, + height_map: Optional[Callable[ + [np.ndarray], Tuple[float, np.ndarray]]] = None, + grid_unit: float = 0.2) -> NodePath: model = GeomNode('floor') node = self.render.attach_new_node(model) - for xi in range(-10, 11): - for yi in range(-10, 11): - tile = GeomNode(f"tile-{xi}.{yi}") - tile.add_geom(geometry.make_plane(size=(1.0, 1.0))) - tile_path = node.attach_new_node(tile) - tile_path.set_pos((xi, yi, 0.0)) - if (xi + yi) % 2: - tile_path.set_color((0.95, 0.95, 1.0, 1)) - else: - tile_path.set_color((0.13, 0.13, 0.2, 1)) + + if height_map is None: + for xi in range(-10, 11): + for yi in range(-10, 11): + tile = GeomNode(f"tile-{xi}.{yi}") + tile.add_geom(geometry.make_plane(size=(1.0, 1.0))) + tile_path = node.attach_new_node(tile) + tile_path.set_pos((xi, yi, 0.0)) + if (xi + yi) % 2: + tile_path.set_color((0.95, 0.95, 1.0, 1)) + else: + tile_path.set_color((0.13, 0.13, 0.2, 1)) + else: + model.add_geom(make_height_map(height_map, 20.0, grid_unit)) + render_attrib = node.get_state().get_attrib_def( + RenderModeAttrib.get_class_slot()) + node.set_attrib(RenderModeAttrib.make( + RenderModeAttrib.M_filled_wireframe, + 0.5, # thickness + render_attrib.perspective, + (0.7, 0.7, 0.7, 1.0) # wireframe_color + )) + node.set_two_sided(True) + return node + def update_floor(self, + height_map: Optional[Callable[ + [np.ndarray], Tuple[float, np.ndarray]]] = None, + grid_unit: float = 0.2) -> NodePath: + if height_map is not None and not callable(height_map): + height_map = pickle.loads(height_map) + self._floor.remove_node() + self._floor = self._make_floor(height_map, grid_unit) + def append_group(self, root_path: str, remove_if_exists: bool = True, diff --git a/python/jiminy_py/src/jiminy_py/viewer/viewer.py b/python/jiminy_py/src/jiminy_py/viewer/viewer.py index 1e83a1acf..5fae1bd07 100644 --- a/python/jiminy_py/src/jiminy_py/viewer/viewer.py +++ b/python/jiminy_py/src/jiminy_py/viewer/viewer.py @@ -15,6 +15,8 @@ import multiprocessing import xml.etree.ElementTree as ET from copy import deepcopy +from urllib.parse import urlparse +from urllib.request import urlopen from functools import wraps, partial from bisect import bisect_right from threading import RLock @@ -478,8 +480,9 @@ def __init__(self, # no other display cell already opened. The user is # probably expecting a display cell to open in such # cases, but there is no fixed rule. - open_gui_if_parent = interactive_mode() and \ - not Viewer._backend_obj.comm_manager.n_comm + open_gui_if_parent = interactive_mode() and ( + Viewer._backend_obj is None or + not Viewer._backend_obj.comm_manager.n_comm) elif Viewer.backend.startswith('panda3d'): open_gui_if_parent = not interactive_mode() else: @@ -510,6 +513,7 @@ def __init__(self, Viewer._backend_robot_colors.update({ self.robot_name: self.robot_color}) except Exception as e: + self.close() raise RuntimeError( "Impossible to create backend or connect to it.") from e @@ -803,7 +807,7 @@ def open_gui(start_if_needed: bool = False) -> bool: Viewer.__connect_backend(start_if_needed) # If a graphical window is already open, do nothing - if Viewer._has_gui: + if Viewer.has_gui(): return if Viewer.backend in ['gepetto-gui', 'panda3d-qt']: @@ -815,24 +819,28 @@ def open_gui(start_if_needed: bool = False) -> bool: viewer_url = Viewer._backend_obj.gui.url() if interactive_mode(): - import urllib from IPython.core.display import HTML, display # Scrap the viewer html content, including javascript # dependencies - html_content = urllib.request.urlopen( - viewer_url).read().decode() + html_content = urlopen(viewer_url).read().decode() pattern = '' scripts_js = re.findall(pattern % '(.*)', html_content) for file in scripts_js: file_path = os.path.join(viewer_url, file) - js_content = urllib.request.urlopen( - file_path).read().decode() + js_content = urlopen(file_path).read().decode() html_content = html_content.replace(pattern % file, f""" """) + # Provide websocket URL as fallback if needed. It would be + # the case if the environment is not jupyter-notebook nor + # colab but rather japyterlab or vscode for instance. + web_url = f"ws://{urlparse(viewer_url).netloc}" + html_content = html_content.replace( + "var ws_url = undefined;", f'var ws_url = "{web_url}";') + if interactive_mode() == 1: # Embed HTML in iframe on Jupyter, since it is not # possible to load HTML/Javascript content directly. @@ -877,6 +885,13 @@ def open_gui(start_if_needed: bool = False) -> bool: @staticmethod def has_gui() -> bool: if Viewer.is_alive(): + # Make sure the viewer still has gui if necessary + if Viewer.backend == 'meshcat': + comm_manager = Viewer._backend_obj.comm_manager + if comm_manager is not None: + ack = Viewer._backend_obj.wait(require_client=False) + Viewer._has_gui = any([ + msg == "meshcat:ok" for msg in ack.split(",")]) return Viewer._has_gui return False diff --git a/python/jiminy_pywrap/src/Controllers.cc b/python/jiminy_pywrap/src/Controllers.cc index 19ba90a87..d70887d1d 100644 --- a/python/jiminy_pywrap/src/Controllers.cc +++ b/python/jiminy_pywrap/src/Controllers.cc @@ -271,8 +271,10 @@ namespace python boost::noncopyable>("BaseController") .def("reset", &AbstractController::reset, &AbstractControllerWrapper::default_reset, (bp::arg("self"), bp::arg("reset_dynamic_telemetry") = false)) - .def("compute_command", bp::pure_virtual(&AbstractController::computeCommand)) - .def("internal_dynamics", bp::pure_virtual(&AbstractController::internalDynamics)); + .def("compute_command", bp::pure_virtual(&AbstractController::computeCommand), + (bp::arg("self"), "t", "q", "v", "command")) + .def("internal_dynamics", bp::pure_virtual(&AbstractController::internalDynamics), + (bp::arg("self"), "t", "q", "v", "u_custom")); } };