diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..951b278 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: + push: + branches: '*' + pull_request: + branches: '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install node + uses: actions/setup-node@v1 + with: + node-version: '10.x' + - name: Install Python + uses: actions/setup-python@v1 + with: + python-version: '3.7' + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install jupyterlab==2.1.0 + python -m pip install ipywidgets + - name: Build the extension + run: | + jlpm && jlpm run build + jupyter labextension install . + python -m jupyterlab.browser_check + - name: Check PyPi and NPM version + run: | + python version_check.py diff --git a/README.md b/README.md index 662028b..bc50744 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ the nbextension: ```bash jupyter nbextension enable --py [--sys-prefix|--user|--system] widget_code_input ``` +There are seven different code themes can be chosen. They are "eclipse", +"idea", "material", "midnight", "monokai", "nord" and "solarized". +You can check the appearance of the code themes at: + +[https://codemirror.net/demo/theme.html](https://codemirror.net/demo/theme.html) + # Acknowlegements diff --git a/css/nord.css b/css/nord.css new file mode 100644 index 0000000..41a8ad7 --- /dev/null +++ b/css/nord.css @@ -0,0 +1,42 @@ +/* Based on arcticicestudio's Nord theme */ +/* https://github.com/arcticicestudio/nord */ + +.cm-s-nord.CodeMirror { background: #2e3440; color: #d8dee9; } +.cm-s-nord div.CodeMirror-selected { background: #434c5e; } +.cm-s-nord .CodeMirror-line::selection, .cm-s-nord .CodeMirror-line > span::selection, .cm-s-nord .CodeMirror-line > span > span::selection { background: #3b4252; } +.cm-s-nord .CodeMirror-line::-moz-selection, .cm-s-nord .CodeMirror-line > span::-moz-selection, .cm-s-nord .CodeMirror-line > span > span::-moz-selection { background: #3b4252; } +.cm-s-nord .CodeMirror-gutters { background: #2e3440; border-right: 0px; } +.cm-s-nord .CodeMirror-guttermarker { color: #4c566a; } +.cm-s-nord .CodeMirror-guttermarker-subtle { color: #4c566a; } +.cm-s-nord .CodeMirror-linenumber { color: #4c566a; } +.cm-s-nord .CodeMirror-cursor { border-left: 1px solid #f8f8f0; } + +.cm-s-nord span.cm-comment { color: #4c566a; } +.cm-s-nord span.cm-atom { color: #b48ead; } +.cm-s-nord span.cm-number { color: #b48ead; } + +.cm-s-nord span.cm-comment.cm-attribute { color: #97b757; } +.cm-s-nord span.cm-comment.cm-def { color: #bc9262; } +.cm-s-nord span.cm-comment.cm-tag { color: #bc6283; } +.cm-s-nord span.cm-comment.cm-type { color: #5998a6; } + +.cm-s-nord span.cm-property, .cm-s-nord span.cm-attribute { color: #8FBCBB; } +.cm-s-nord span.cm-keyword { color: #81A1C1; } +.cm-s-nord span.cm-builtin { color: #81A1C1; } +.cm-s-nord span.cm-string { color: #A3BE8C; } + +.cm-s-nord span.cm-variable { color: #d8dee9; } +.cm-s-nord span.cm-variable-2 { color: #d8dee9; } +.cm-s-nord span.cm-variable-3, .cm-s-nord span.cm-type { color: #d8dee9; } +.cm-s-nord span.cm-def { color: #8FBCBB; } +.cm-s-nord span.cm-bracket { color: #81A1C1; } +.cm-s-nord span.cm-tag { color: #bf616a; } +.cm-s-nord span.cm-header { color: #b48ead; } +.cm-s-nord span.cm-link { color: #b48ead; } +.cm-s-nord span.cm-error { background: #bf616a; color: #f8f8f0; } + +.cm-s-nord .CodeMirror-activeline-background { background: #3b4252; } +.cm-s-nord .CodeMirror-matchingbracket { + text-decoration: underline; + color: white !important; +} diff --git a/demos/explanation.png b/demos/explanation.png new file mode 100644 index 0000000..c124ace Binary files /dev/null and b/demos/explanation.png differ diff --git a/demos/explanation.svg b/demos/explanation.svg new file mode 100644 index 0000000..569eb4a --- /dev/null +++ b/demos/explanation.svg @@ -0,0 +1,323 @@ + + + + diff --git a/demos/index.ipynb b/demos/index.ipynb new file mode 100644 index 0000000..fea67bf --- /dev/null +++ b/demos/index.ipynb @@ -0,0 +1,69 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# widget-code-input demos\n", + "\n", + "You can visualize here a few demos of the `widget-code-input` widget for Jupyter, a widget that allows you to write snippets of code in the form of a function, and then test them.\n", + "\n", + "This widget is best used in combination with the [appmode](https://pypi.org/project/appmode/) plugin for Jupyter.\n", + "\n", + "Click on the headers to go to the respective demo." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## [Simple showcase](./showcase.ipynb)\n", + "This demo is a simple showcase of the functionality of this widget.\n", + "It shows:\n", + "\n", + "- how to use the widget\n", + "- how to get and set the content of the function body from python\n", + "- how to monitor events from python (e.g. perform on operation in python every time the code is changed by the user)\n", + "- that it is possible in insert multiple, independent widgets in the same notebook\n", + "- how to obtain the function object from the widget and how to run it" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercise: compute the point at which a projectile would hit the ground \n", + "### [Non-zooming version](./projectile-inline.ipynb) - [Interactive version](./projectile-notebook.ipynb)\n", + "This demo shows a possible exercise for students: given the initial parameters of a projectile (height of launch, velocity), ask the student to write the function to compute the position where the projectile will hit the ground." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demos/projectile-inline.ipynb b/demos/projectile-inline.ipynb new file mode 100644 index 0000000..07a5183 --- /dev/null +++ b/demos/projectile-inline.ipynb @@ -0,0 +1,441 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Exercise: computing the distance at which a projectile will hit the ground\n", + "\n", + "**Author: Giovanni Pizzi, EPFL**\n", + "\n", + "In this exercise, you are given three parameters, defining the initial conditions at which a projectile is launched. \n", + "In particular, you are given in input:\n", + "\n", + "- the height $h$ above the ground from which the projectile is launched\n", + "- the two components (horizontal $v_x$ and vertical $v_y$) of the velocity, $\\vec v = (v_x, v_y)$ at which the projectile is launched\n", + "\n", + "\n", + "\n", + "## Task\n", + "**Your task is to write a python function that, given these three parameters, computes the horizontal position $D$ at which the projectile will hit the ground.**\n", + "\n", + "## How to test the results\n", + "To test your function, you can move the sliders below that determine the initial conditions of the projectile.\n", + "\n", + "A real-time visualization will show the correct solution for the problem (solid curve), where the launch point is marked by a black dot and the correct hitting point by a black cross.\n", + "\n", + "You will also see the result of your proposed solution as a large red circle. Finally, You can inspect possible errors of your function by opening the tab \"Results of the validation of your function\"." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Bad key \"text.kerning_factor\" on line 4 in\n", + "/home/dou/anaconda3/envs/jlab2.0/lib/python3.8/site-packages/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle.\n", + "You probably need to get an updated matplotlibrc file from\n", + "https://github.com/matplotlib/matplotlib/blob/v3.1.3/matplotlibrc.template\n", + "or from the matplotlib source distribution\n" + ] + } + ], + "source": [ + "%matplotlib inline\n", + "import numpy as np\n", + "import pylab as pl\n", + "\n", + "import tabulate\n", + "from ipywidgets import Label, Button, Output, FloatSlider, HBox, VBox, Layout, HTML, Accordion\n", + "from widget_code_input import WidgetCodeInput\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Value of the vertical (downwards) acceleration\n", + "g = 9.81 # m/s^2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "432120a79fc64d1e84abf87e29316d15", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "WidgetCodeInput(docstring=\"\\nA function to compute the hit coordinate of a projectile \\non the ground, knowing…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "code_widget = WidgetCodeInput(\n", + " function_name=\"get_hit_coordinate\", \n", + " function_parameters=\"vertical_position, horizontal_v, vertical_v, g={}\".format(g),\n", + " docstring=\"\"\"\n", + "A function to compute the hit coordinate of a projectile \n", + "on the ground, knowing the initial launch parameters.\n", + "\n", + ":param vertical_position: launch vertical position [m]\n", + ":param horizontal_v: launch horizontal position [m/s]\n", + ":param vertical_v: launch vertical position [m/s] \n", + " (positive values means upward velocity)\n", + ":param g: the vertical (downwards) acceleration (default: Earth's gravity)\n", + " \n", + ":return: the position at which the projectile will hit the ground [m]\n", + "\"\"\",\n", + " function_body=\"# Input here your solution\\n# After changing the function, move one of the sliders to validate your function\")\n", + "display(code_widget)\n", + "\n", + "## The solution:\n", + "# import math\n", + "# return horizontal_v * (vertical_v + math.sqrt(vertical_v**2 + 2. * g * vertical_position)) / g" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "43240995570947849c995deb2b44c9df", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(VBox(children=(FloatSlider(value=6.0, continuous_update=False, description='Vertical position […" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vertical_position_widget = FloatSlider(\n", + " value=6, min=0, max=10, \n", + " description=\"Vertical position [m]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "horizontal_v_widget = FloatSlider(\n", + " value=5, min=-10, max=10, \n", + " description=\"Horizontal velocity [m/s]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "vertical_v_widget = FloatSlider(\n", + " value=3, min=-10, max=10, \n", + " description=\"Vertical velocity [m/s]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "\n", + "plot_box = Output()\n", + "\n", + "input_box = VBox([vertical_position_widget, horizontal_v_widget, vertical_v_widget])\n", + "display(HBox([input_box, plot_box]))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6376783bb9904c70a1dde34f230567b8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Accordion(children=(Output(),), selected_index=None, _titles={'0': 'Results of the validation of your function…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "check_function_output = Output()\n", + "check_accordion = Accordion(children=[check_function_output], selected_index=None)\n", + "check_accordion.set_title(0, 'Results of the validation of your function (click here to see them)')\n", + "\n", + "display(check_accordion)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def trajectory(t, vertical_position, horizontal_v, vertical_v, g):\n", + " \"\"\"\n", + " Return the coordinates (x, y) at time t\n", + " \"\"\"\n", + " # We define the initial x coordinate to be zero\n", + " x0 = 0\n", + " \n", + " x = x0 + horizontal_v * t\n", + " y = -0.5 * g* t**2 + vertical_v * t + vertical_position\n", + " \n", + " return x, y \n", + "\n", + "def hit_conditions(vertical_position, horizontal_v, vertical_v, g):\n", + " \"\"\"\n", + " Return (t, D), where t is the time at which the ground is hit, and D \n", + " is the distance at which the projectile hits the ground\n", + " \"\"\"\n", + " \n", + " # We define the initial x coordinate to be zero\n", + " x0 = 0\n", + " \n", + " # x = x0 + horizontal_v * t => t = (x-x0) / horizontal_v\n", + " # y = -0.5 * g* t**2 + vertical_v * t + vertical_position => \n", + " #\n", + " # y == 0 => \n", + " a = -0.5 * g\n", + " b = vertical_v\n", + " c = vertical_position\n", + " \n", + " # the two solutions; I want the solution with positive t, \n", + " # that will in any case be t1, because\n", + " # t1 > t2 for any value of a, b, c (since a < 0)\n", + " t1 = (-b - np.sqrt(b**2 - 4 * a * c)) / (2. * a)\n", + " #t2 = (-b + np.sqrt(b**2 - 4 * a * c)) / (2. * a)\n", + " \n", + " t = t1\n", + " \n", + " D = x0 + horizontal_v * t\n", + " \n", + " return t, D\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def check_user_value():\n", + " # I don't catch exceptions so that the users can see the traceback\n", + " error_string = \"YOUR FUNCTION DOES NOT SEEM RIGHT, PLEASE TRY TO FIX IT\"\n", + " ok_string = \"YOUR FUNCTION SEEMS TO BE CORRECT!! CONGRATULATIONS!\"\n", + " \n", + " test_table = []\n", + " last_exception = None\n", + " type_warning = False\n", + " \n", + " check_function_output.clear_output(wait=True)\n", + " with check_function_output:\n", + " user_function = code_widget.get_function_object() \n", + "\n", + " test_values_vpos = range(1,7)\n", + " test_values_vx = range(-2,3)\n", + " test_values_vy = range(-2,3) \n", + " for test_vpos in test_values_vpos:\n", + " for test_vx in test_values_vx:\n", + " for test_vy in test_values_vy:\n", + " correct_value = hit_conditions(vertical_position=test_vpos, \n", + " horizontal_v=test_vx,\n", + " vertical_v=test_vy,\n", + " g=g\n", + " )[1] # [1] because this gives D ([0] is instead t_hit)\n", + " try:\n", + " user_hit_position = user_function(\n", + " vertical_position=test_vpos, \n", + " horizontal_v=test_vx,\n", + " vertical_v=test_vy,\n", + " )\n", + " try:\n", + " error = abs(user_hit_position - correct_value)\n", + " except Exception:\n", + " type_warning = True\n", + " error = 1. # Large value so it triggers a failed test\n", + " except Exception as exc:\n", + " last_exception = exc\n", + " test_table.append([test_vpos, test_vx, test_vy, correct_value, \"ERROR\", False])\n", + " else:\n", + " if error > 1.e-8:\n", + " test_table.append([test_vpos, test_vx, test_vy, str(correct_value), str(user_hit_position), False])\n", + " else:\n", + " test_table.append([test_vpos, test_vx, test_vy, str(correct_value), str(user_hit_position), True])\n", + "\n", + " num_tests = len(test_table)\n", + " num_passed_tests = len([test for test in test_table if test[5]])\n", + " failed_tests = [test[:-1] for test in test_table if not test[5]] # Keep only failed tests, and remove last column\n", + " MAX_FAILED_TESTS = 5\n", + " if num_passed_tests < num_tests:\n", + " html_table = HTML(\"\" + \n", + " tabulate.tabulate(\n", + " failed_tests[:MAX_FAILED_TESTS], \n", + " tablefmt='html',\n", + " headers=[\"vertical_position\", \"horizontal_v\", \"vertical_v\", \"Expected value\", \"Your value\"]\n", + " ))\n", + " \n", + " if num_passed_tests < num_tests:\n", + " print(\"Your function does not seem correct; only {}/{} tests passed\".format(num_passed_tests, num_tests))\n", + " print(\"Printing up to {} failed tests:\".format(MAX_FAILED_TESTS))\n", + " display(html_table)\n", + " else:\n", + " print(\"Your function is correct! Very good! All {} tests passed\".format(num_tests))\n", + " \n", + " if type_warning:\n", + " print(\"WARNING! in at least one case, your function did not return a valid float number, please double check!\".format(num_tests))\n", + " \n", + " # Raise the last exception obtained\n", + " if last_exception is not None:\n", + " print(\"I obtained at least one exception\")\n", + " raise last_exception from None\n", + " \n", + "def get_user_value():\n", + " \"\"\"\n", + " This function returns the value computed by the user's\n", + " function for the current sliders' value, or None if there is an exception\n", + " \"\"\"\n", + " with check_function_output:\n", + " user_function = code_widget.get_function_object() \n", + " try:\n", + " user_hit_position = user_function(\n", + " vertical_position=vertical_position_widget.value, \n", + " horizontal_v=horizontal_v_widget.value,\n", + " vertical_v=vertical_v_widget.value,\n", + " )\n", + " except Exception as exc:\n", + " return None\n", + " return user_hit_position " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def replot(vertical_position, horizontal_v, vertical_v):\n", + " #global the_figure, the_plot, g\n", + " global g\n", + " \n", + " the_figure = pl.figure(figsize=(4,3))\n", + " the_plot = pl.subplot(1,1,1)\n", + " pl.xlabel(\"x [m]\")\n", + " pl.xlabel(\"y [m]\")\n", + " \n", + " \n", + " # Compute correct values\n", + " t_hit, D = hit_conditions(vertical_position, horizontal_v, vertical_v, g)\n", + " t_array = np.linspace(0,t_hit, 100)\n", + " x_array, y_array = trajectory(t_array, vertical_position, horizontal_v, vertical_v, g)\n", + "\n", + " # Plot orrect curves and points\n", + " pl.plot([0], [vertical_position], 'ok')\n", + " pl.plot([D], [0], 'xk') \n", + " pl.plot(x_array, y_array, '-b')\n", + "\n", + " \n", + " ## (Try to) plot user value\n", + " user_value = None\n", + " try:\n", + " user_value = get_user_value()\n", + " except Exception:\n", + " # Just a guard not to break the visualization, we should not end up here\n", + " pass \n", + " try:\n", + " if user_value is not None:\n", + " the_plot.plot([user_value], [0], 'or') \n", + " except Exception:\n", + " # We might end up here if the function does not return a float value\n", + " pass \n", + "\n", + " pl.axhline(0, color='gray')\n", + " # Set zoom to fixed value\n", + " the_plot.set_xlim([-30, 30])\n", + " the_plot.set_ylim([-1, 16])\n", + " \n", + " # Redraw\n", + " pl.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def recompute(e):\n", + " global plot_box, g\n", + " \n", + " if e is not None:\n", + " if e['type'] != 'change' or e['name'] not in ['value', 'function_body']:\n", + " return \n", + " plot_box.clear_output(wait=True)\n", + " with plot_box:\n", + " replot(\n", + " vertical_position=vertical_position_widget.value, \n", + " horizontal_v=horizontal_v_widget.value,\n", + " vertical_v=vertical_v_widget.value,\n", + " )\n", + " \n", + " # Print info on the \"correctness\" of the user's function\n", + " check_user_value()\n", + " \n", + "# Bind the sliders to the event\n", + "vertical_position_widget.observe(recompute)\n", + "horizontal_v_widget.observe(recompute)\n", + "vertical_v_widget.observe(recompute)\n", + "\n", + "# Bind also the code widget\n", + "code_widget.observe(recompute)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Perform the first recomputation (to create the plot)\n", + "_ = recompute(None)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/projectile-notebook.ipynb b/demos/projectile-notebook.ipynb new file mode 100644 index 0000000..b54b508 --- /dev/null +++ b/demos/projectile-notebook.ipynb @@ -0,0 +1,441 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Exercise: computing the distance at which a projectile will hit the ground\n", + "\n", + "**Author: Giovanni Pizzi, EPFL**\n", + "\n", + "In this exercise, you are given three parameters, defining the initial conditions at which a projectile is launched. \n", + "In particular, you are given in input:\n", + "\n", + "- the height $h$ above the ground from which the projectile is launched\n", + "- the two components (horizontal $v_x$ and vertical $v_y$) of the velocity, $\\vec v = (v_x, v_y)$ at which the projectile is launched\n", + "\n", + "\n", + "\n", + "## Task\n", + "**Your task is to write a python function that, given these three parameters, computes the horizontal position $D$ at which the projectile will hit the ground.**\n", + "\n", + "## How to test the results\n", + "To test your function, you can move the sliders below that determine the initial conditions of the projectile.\n", + "\n", + "A real-time visualization will show the correct solution for the problem (solid curve), where the launch point is marked by a black dot and the correct hitting point by a black cross.\n", + "\n", + "You will also see the result of your proposed solution as a large red circle. Finally, You can inspect possible errors of your function by opening the tab \"Results of the validation of your function\"." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Bad key \"text.kerning_factor\" on line 4 in\n", + "/home/dou/anaconda3/envs/jlab2.0/lib/python3.8/site-packages/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle.\n", + "You probably need to get an updated matplotlibrc file from\n", + "https://github.com/matplotlib/matplotlib/blob/v3.1.3/matplotlibrc.template\n", + "or from the matplotlib source distribution\n" + ] + } + ], + "source": [ + "%matplotlib notebook\n", + "import numpy as np\n", + "import pylab as pl\n", + "\n", + "import tabulate\n", + "from ipywidgets import Label, Button, Output, FloatSlider, HBox, VBox, Layout, HTML, Accordion\n", + "from widget_code_input import WidgetCodeInput\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Value of the vertical (downwards) acceleration\n", + "g = 9.81 # m/s^2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "31d644fb7933439fa91dd2c272d9eeb9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "WidgetCodeInput(docstring=\"\\nA function to compute the hit coordinate of a projectile \\non the ground, knowing…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "code_widget = WidgetCodeInput(\n", + " function_name=\"get_hit_coordinate\", \n", + " function_parameters=\"vertical_position, horizontal_v, vertical_v, g={}\".format(g),\n", + " docstring=\"\"\"\n", + "A function to compute the hit coordinate of a projectile \n", + "on the ground, knowing the initial launch parameters.\n", + "\n", + ":param vertical_position: launch vertical position [m]\n", + ":param horizontal_v: launch horizontal position [m/s]\n", + ":param vertical_v: launch vertical position [m/s] \n", + " (positive values means upward velocity)\n", + ":param g: the vertical (downwards) acceleration (default: Earth's gravity)\n", + " \n", + ":return: the position at which the projectile will hit the ground [m]\n", + "\"\"\",\n", + " function_body=\"# Input here your solution\\n# After changing the function, move one of the sliders to validate your function\")\n", + "display(code_widget)\n", + "\n", + "## The solution:\n", + "# import math\n", + "# return horizontal_v * (vertical_v + math.sqrt(vertical_v**2 + 2. * g * vertical_position)) / g" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3383ae2a018a4bb89706a94f2096489f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(VBox(children=(FloatSlider(value=6.0, continuous_update=False, description='Vertical position […" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vertical_position_widget = FloatSlider(\n", + " value=6, min=0, max=10, \n", + " description=\"Vertical position [m]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "horizontal_v_widget = FloatSlider(\n", + " value=5, min=-10, max=10, \n", + " description=\"Horizontal velocity [m/s]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "vertical_v_widget = FloatSlider(\n", + " value=3, min=-10, max=10, \n", + " description=\"Vertical velocity [m/s]\",\n", + " continuous_update=False, \n", + " style={'description_width': 'initial'}, layout=Layout(width='50%', min_width='350px'))\n", + "\n", + "plot_box = Output()\n", + "\n", + "input_box = VBox([vertical_position_widget, horizontal_v_widget, vertical_v_widget])\n", + "display(HBox([input_box, plot_box]))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5d3bc4a5bfd14bd8a2c7225bed6108d1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Accordion(children=(Output(),), selected_index=None, _titles={'0': 'Results of the validation of your function…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "check_function_output = Output()\n", + "check_accordion = Accordion(children=[check_function_output], selected_index=None)\n", + "check_accordion.set_title(0, 'Results of the validation of your function (click here to see them)')\n", + "\n", + "display(check_accordion)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def trajectory(t, vertical_position, horizontal_v, vertical_v, g):\n", + " \"\"\"\n", + " Return the coordinates (x, y) at time t\n", + " \"\"\"\n", + " # We define the initial x coordinate to be zero\n", + " x0 = 0\n", + " \n", + " x = x0 + horizontal_v * t\n", + " y = -0.5 * g* t**2 + vertical_v * t + vertical_position\n", + " \n", + " return x, y \n", + "\n", + "def hit_conditions(vertical_position, horizontal_v, vertical_v, g):\n", + " \"\"\"\n", + " Return (t, D), where t is the time at which the ground is hit, and D \n", + " is the distance at which the projectile hits the ground\n", + " \"\"\"\n", + " \n", + " # We define the initial x coordinate to be zero\n", + " x0 = 0\n", + " \n", + " # x = x0 + horizontal_v * t => t = (x-x0) / horizontal_v\n", + " # y = -0.5 * g* t**2 + vertical_v * t + vertical_position => \n", + " #\n", + " # y == 0 => \n", + " a = -0.5 * g\n", + " b = vertical_v\n", + " c = vertical_position\n", + " \n", + " # the two solutions; I want the solution with positive t, \n", + " # that will in any case be t1, because\n", + " # t1 > t2 for any value of a, b, c (since a < 0)\n", + " t1 = (-b - np.sqrt(b**2 - 4 * a * c)) / (2. * a)\n", + " #t2 = (-b + np.sqrt(b**2 - 4 * a * c)) / (2. * a)\n", + " \n", + " t = t1\n", + " \n", + " D = x0 + horizontal_v * t\n", + " \n", + " return t, D\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def check_user_value():\n", + " # I don't catch exceptions so that the users can see the traceback\n", + " error_string = \"YOUR FUNCTION DOES NOT SEEM RIGHT, PLEASE TRY TO FIX IT\"\n", + " ok_string = \"YOUR FUNCTION SEEMS TO BE CORRECT!! CONGRATULATIONS!\"\n", + " \n", + " test_table = []\n", + " last_exception = None\n", + " type_warning = False\n", + " \n", + " check_function_output.clear_output(wait=True)\n", + " with check_function_output:\n", + " user_function = code_widget.get_function_object() \n", + "\n", + " test_values_vpos = range(1,7)\n", + " test_values_vx = range(-2,3)\n", + " test_values_vy = range(-2,3) \n", + " for test_vpos in test_values_vpos:\n", + " for test_vx in test_values_vx:\n", + " for test_vy in test_values_vy:\n", + " correct_value = hit_conditions(vertical_position=test_vpos, \n", + " horizontal_v=test_vx,\n", + " vertical_v=test_vy,\n", + " g=g\n", + " )[1] # [1] because this gives D ([0] is instead t_hit)\n", + " try:\n", + " user_hit_position = user_function(\n", + " vertical_position=test_vpos, \n", + " horizontal_v=test_vx,\n", + " vertical_v=test_vy,\n", + " )\n", + " try:\n", + " error = abs(user_hit_position - correct_value)\n", + " except Exception:\n", + " type_warning = True\n", + " error = 1. # Large value so it triggers a failed test\n", + " except Exception as exc:\n", + " last_exception = exc\n", + " test_table.append([test_vpos, test_vx, test_vy, correct_value, \"ERROR\", False])\n", + " else:\n", + " if error > 1.e-8:\n", + " test_table.append([test_vpos, test_vx, test_vy, str(correct_value), str(user_hit_position), False])\n", + " else:\n", + " test_table.append([test_vpos, test_vx, test_vy, str(correct_value), str(user_hit_position), True])\n", + "\n", + " num_tests = len(test_table)\n", + " num_passed_tests = len([test for test in test_table if test[5]])\n", + " failed_tests = [test[:-1] for test in test_table if not test[5]] # Keep only failed tests, and remove last column\n", + " MAX_FAILED_TESTS = 5\n", + " if num_passed_tests < num_tests:\n", + " html_table = HTML(\"\" + \n", + " tabulate.tabulate(\n", + " failed_tests[:MAX_FAILED_TESTS], \n", + " tablefmt='html',\n", + " headers=[\"vertical_position\", \"horizontal_v\", \"vertical_v\", \"Expected value\", \"Your value\"]\n", + " ))\n", + " \n", + " if num_passed_tests < num_tests:\n", + " print(\"Your function does not seem correct; only {}/{} tests passed\".format(num_passed_tests, num_tests))\n", + " print(\"Printing up to {} failed tests:\".format(MAX_FAILED_TESTS))\n", + " display(html_table)\n", + " else:\n", + " print(\"Your function is correct! Very good! All {} tests passed\".format(num_tests))\n", + " \n", + " if type_warning:\n", + " print(\"WARNING! in at least one case, your function did not return a valid float number, please double check!\".format(num_tests))\n", + " \n", + " # Raise the last exception obtained\n", + " if last_exception is not None:\n", + " print(\"I obtained at least one exception\")\n", + " raise last_exception from None\n", + " \n", + "def get_user_value():\n", + " \"\"\"\n", + " This function returns the value computed by the user's\n", + " function for the current sliders' value, or None if there is an exception\n", + " \"\"\"\n", + " with check_function_output:\n", + " user_function = code_widget.get_function_object() \n", + " try:\n", + " user_hit_position = user_function(\n", + " vertical_position=vertical_position_widget.value, \n", + " horizontal_v=horizontal_v_widget.value,\n", + " vertical_v=vertical_v_widget.value,\n", + " )\n", + " except Exception as exc:\n", + " return None\n", + " return user_hit_position " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "with plot_box:\n", + " the_figure = pl.figure(figsize=(4,3))\n", + " the_plot = the_figure.add_subplot(1,1,1)\n", + " the_plot.set_xlabel(\"x [m]\")\n", + " the_plot.set_xlabel(\"y [m]\")\n", + "\n", + "def replot(vertical_position, horizontal_v, vertical_v):\n", + " global the_plot, g\n", + " \n", + " # Compute correct values\n", + " t_hit, D = hit_conditions(vertical_position, horizontal_v, vertical_v, g)\n", + " t_array = np.linspace(0,t_hit, 100)\n", + " x_array, y_array = trajectory(t_array, vertical_position, horizontal_v, vertical_v, g)\n", + "\n", + " # Clean up the graph\n", + " the_plot.axes.clear()\n", + " # Plot orrect curves and points\n", + " the_plot.plot([0], [vertical_position], 'ok')\n", + " the_plot.plot([D], [0], 'xk') \n", + " the_plot.plot(x_array, y_array, '-b')\n", + "\n", + " \n", + " ## (Try to) plot user value\n", + " user_value = None\n", + " try:\n", + " user_value = get_user_value()\n", + " except Exception:\n", + " # Just a guard not to break the visualization, we should not end up here\n", + " pass \n", + " try:\n", + " if user_value is not None:\n", + " the_plot.plot([user_value], [0], 'or') \n", + " except Exception:\n", + " # We might end up here if the function does not return a float value\n", + " pass \n", + "\n", + " the_plot.axhline(0, color='gray')\n", + " # Set zoom to fixed value\n", + " the_plot.set_xlim([-30, 30])\n", + " the_plot.set_ylim([-1, 16])\n", + " \n", + " # Redraw\n", + " the_figure.canvas.draw()\n", + " the_figure.canvas.flush_events()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def recompute(e):\n", + " global the_plot, g\n", + " \n", + " if e is not None:\n", + " if e['type'] != 'change' or e['name'] not in ['value', 'function_body']:\n", + " return \n", + " replot(\n", + " vertical_position=vertical_position_widget.value, \n", + " horizontal_v=horizontal_v_widget.value,\n", + " vertical_v=vertical_v_widget.value,\n", + " )\n", + " \n", + " # Print info on the \"correctness\" of the user's function\n", + " check_user_value()\n", + " \n", + "# Bind the sliders to the event\n", + "vertical_position_widget.observe(recompute)\n", + "horizontal_v_widget.observe(recompute)\n", + "vertical_v_widget.observe(recompute)\n", + "\n", + "# Bind also the code widget\n", + "code_widget.observe(recompute)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Perform the first recomputation (to create the plot)\n", + "_ = recompute(None)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/showcase.ipynb b/demos/showcase.ipynb new file mode 100644 index 0000000..d846974 --- /dev/null +++ b/demos/showcase.ipynb @@ -0,0 +1,297 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from widget_code_input import WidgetCodeInput\n", + "from IPython.display import display, Markdown\n", + "from ipywidgets import Label, Button, Output, Layout" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2c3091cf07b04074a76e6049f6ad6791", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "WidgetCodeInput(function_name='is_even', function_parameters='number')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "widget = WidgetCodeInput(function_name=\"is_even\", function_parameters=\"number\")\n", + "display(widget)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "widget.function_body = 'if number % 2:\\n return False\\nreturn True'" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d96cc93702304310a94909e536970f5d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "content=Output()\n", + "display(content)\n", + "with content:\n", + " display(Markdown('Reprinting here below the function body:\\n\\n```python\\n{}\\n```'.format(widget.full_function_code)))\n", + "\n", + "def update_label(event):\n", + " global content, widget\n", + " if event['type'] == 'change':# and event['name'] == 'function_body': # Removed this part because also e.g. the docstring could change\n", + " content.clear_output(wait=True) # wait=True prevents flickering\n", + " with content:\n", + " #display(Markdown('Reprinting here below the function body:\\n\\n```python\\n{}\\n```'.format(event['new'])))\n", + " display(Markdown('Reprinting here below the function body:\\n\\n```python\\n{}\\n```'.format(widget.full_function_code)))\n", + " \n", + "widget.observe(update_label)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3eecdc05d09d4f8fbe5216e34aabbb24", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Check function results', layout=Layout(width='300px'), style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "807177d37f9c462c82c84c91188b4b45", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "check_function_output = Output()\n", + "\n", + "def check_function(clicked_widget):\n", + " global widget\n", + " check_function_output.clear_output(wait=True)\n", + " with check_function_output:\n", + " user_function = widget.get_function_object() # This could raise\n", + " for i in range(7):\n", + " print(\"Is {} even? {}\".format(i, user_function(i)))\n", + "\n", + "button = Button(description=\"Check function results\", layout=Layout(width='300px'))\n", + "button.on_click(check_function)\n", + "\n", + "display(button)\n", + "display(check_function_output)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "