{ "cells": [ { "cell_type": "raw", "id": "0", "metadata": { "editable": true, "raw_mimetype": "text/restructuredtext", "slideshow": { "slide_type": "" }, "tags": [], "vscode": { "languageId": "raw" } }, "source": [ ".. important::\n", " Jupyter widgets cannot be run within the documentation. To interact with the widget, you must run a mybinder instance. To run a mybinder instance of this notebook, please use this link https://mybinder.org/v2/gh/osscar-org/scicode-widgets/HEAD?labpath=docs%2Fsrc%2Fexercises.ipynb. Note that also the LaTeX rendering is resolved when running the notebook." ] }, { "cell_type": "markdown", "id": "1", "metadata": { "tags": [] }, "source": [ "# Writing exercises" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "scicode-widgets provides a flexible code widget that allows instant feedback to evaluate the code for interactive plots." ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": { "tags": [] }, "outputs": [], "source": [ "from scwidgets import (\n", " CodeExercise,\n", " CodeInput,\n", " CueFigure,\n", " CueOutput,\n", " CueObject,\n", " ExerciseRegistry,\n", " ParametersPanel,\n", " TextExercise,\n", ")\n", "\n", "from ipywidgets import FloatSlider, IntSlider\n", "import matplotlib.pyplot as plt\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "## ExerciseRegistry to store and load answers to exercises" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "Due to limitations of jupyter notebooks in saving the widgets state, we store and load the widget state by ourselves using the `ExerciseRegistry`. It allows you specify a filename for the JSON file to store all the answers to all registered exercises. The exercises are registered by passing the `ExerciseRegistry` instance as input argument." ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": { "tags": [] }, "outputs": [], "source": [ "exercise_registry = ExerciseRegistry()\n", "exercise_registry" ] }, { "cell_type": "markdown", "id": "7", "metadata": {}, "source": [ "## TextExercise to create text exercises" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "A simple text area to save students' answers. Note that the save and load buttons only appear when an exercise key and registry are given." ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": { "tags": [] }, "outputs": [], "source": [ "TextExercise(\n", " title=\"Exercise 01: Derive a solution for weights\",\n", " description=\"\"\"\n", " We can define ridge regression by extending the ordinary least\n", " square solution by a penalization $\\lambda\\|\\mathbf{w}\\|_2^2$. Please derive the solution\n", " for the weights from the optimization problem:

\n", " $$\\hat{\\mathbf{w}} = \\min_\\mathbf{w} \\|\\mathbf{y}-\\mathbf{X}\\mathbf{w}\\|^2 + \\lambda\\|\\mathbf{w}\\|^2$$\"\"\",\n", " key=\"ex01\", # the key it is stored under in the json file\n", " exercise_registry=exercise_registry\n", ")" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "## Interactive coding exercises" ] }, { "cell_type": "markdown", "id": "11", "metadata": {}, "source": [ "This is an example on how to create a simple exercise." ] }, { "cell_type": "code", "execution_count": null, "id": "12", "metadata": {}, "outputs": [], "source": [ "# This is what the students sees and can adapt\n", "def sin(x: int, omega=1.0):\n", " \"\"\"\n", " Implements ridge regression\n", "\n", " :param x: An array of data points\n", " :param omega: The frequency\n", " \"\"\"\n", " import numpy as np\n", " return np.sin(x*omega)\n", "\n", "def update_func(code_ex):\n", " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n", " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n", " ax = code_ex.figure.gca()\n", " ax.plot(x, y)\n", " \n", "code_ex_description = \"\"\"\n", "Implements a sinus function $\\sin(x\\omega)$.\n", "\"\"\"\n", "code_ex = CodeExercise(\n", " code=sin,\n", " parameters={'omega': (0.5, 3.14, 0.1)},\n", " outputs=plt.figure(),\n", " update=update_func,\n", " update_mode=\"continuous\", # we also support [\"manual\", \"release\"]\n", " title=\"Sinus function\",\n", " description=code_ex_description,\n", " key=\"sin_local\",\n", " exercise_registry=exercise_registry\n", ")\n", "\n", "code_ex.run_update() # For the demonstration we run the widget one time\n", "display(code_ex)" ] }, { "cell_type": "markdown", "id": "13", "metadata": {}, "source": [ "### Creating widget components beforehand" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "More complex widgets might need to be created beforehand to allow full customization" ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": { "tags": [] }, "outputs": [], "source": [ "from ipywidgets import HTML\n", "\n", "# One can pass a function also by\n", "code_input = CodeInput(\n", " function_name=\"sin\",\n", " function_parameters=\"x: int, omega=1.0\",\n", " function_body=\"import numpy as np\\nreturn np.sin(x*omega)\",\n", " docstring=\"Implements ridge regression\\n\\n:param x: An array of data points\\n:param omega: The frequency\"\n", ")\n", "# customization of figure toolbar, only important in widget mode (use %matplotlib widget)\n", "figure = CueFigure(plt.figure(), show_toolbars=True)\n", "# to use a custom output for own widgets\n", "output = CueOutput()\n", "# to use display custom widgets\n", "table = CueObject(HTML(value=\"
xy
\"))\n", "\n", "\n", "# to customize sliders, one can directy\n", "parameter_panel = ParametersPanel(\n", " omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n", ")\n", "# alternatively if passed to CodeExercise this also works\n", "#parameter_panel = dict(\n", "# omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n", "#)\n", "\n", "\n", "def update_func(code_ex):\n", " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n", " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n", " ax = code_ex.outputs[0].figure.gca()\n", " ax.plot(x, y)\n", " with code_ex.outputs[1]:\n", " print(\"Some text after the figure\")\n", "\n", " code_ex.outputs[2].object.value = \"\" + \\\n", " \"\".join([f\"\" for i in range(0, len(x), 20)]) + \\\n", " \"
xy
{x[i]:.2f}{y[i]:.2f}
\"\n", " # the captured text in the function is always printed before any other output\n", " print(\"Some text before the figure\")\n", " \n", " \n", "\n", "code_ex = CodeExercise(\n", " code=code_input,\n", " parameters=parameter_panel,\n", " outputs=[figure, output, table],\n", " update=update_func,\n", ")\n", "\n", "code_ex.run_update() # For the demonstration we run the widget one time\n", "display(code_ex)" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "### Include imports to code input" ] }, { "cell_type": "markdown", "id": "17", "metadata": {}, "source": [ "So far we always added the imports required for the code inside the code input. We need to do this because the widget creates its own environment (own globals), so no function from the notebook is accidently used. However, if you want to already provide imports to the user without specifiying them or need them for example for typehints, you can add the library to the `builtins`." ] }, { "cell_type": "code", "execution_count": null, "id": "18", "metadata": {}, "outputs": [], "source": [ "def sin(x: np.ndarray, omega=1.0): # using np.ndarray requires import numpy before the function body\n", " \"\"\"\n", " Implements ridge regression\n", "\n", " :param x: An array of data points\n", " :param omega: The frequency\n", " \"\"\"\n", " return np.sin(x*omega)\n", "\n", "def update_func(code_ex):\n", " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n", " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n", " ax = code_ex.figure.gca()\n", " ax.plot(x, y)\n", " \n", "code_ex = CodeExercise(\n", " code=CodeInput(sin, builtins={'np': np}),\n", " parameters={'omega': (0.5, 3.14, 0.1)},\n", " outputs=plt.figure(),\n", " update=update_func,\n", ")\n", "\n", "code_ex.run_update() # For the demonstration we run the widget one time\n", "display(code_ex)" ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "### Interactive coding exercises with globals variables" ] }, { "cell_type": "markdown", "id": "20", "metadata": {}, "source": [ "This is an example how to create a simple exercise using globals in the update function. This can be more convenient in certain cases but a bit more prone to errors since when creating multiple exercises the global names can easily conflict with each other and result in unwanted behavior. Therefore we recommend that the code demo instance is used through the update function argument." ] }, { "cell_type": "code", "execution_count": null, "id": "21", "metadata": { "tags": [] }, "outputs": [], "source": [ "# This is what the students sees and can adapt\n", "def sin(x, omega):\n", " \"\"\"\n", " Implements ridge regression\n", "\n", " :param x: An array of data points\n", " :param omega: The frequency\n", " \"\"\"\n", " import numpy as np\n", " return np.sin(x*omega)\n", "\n", "code_input = CodeInput(sin)\n", "cue_figure = CueFigure(plt.figure())\n", "parameter_panel = ParametersPanel(\n", " omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n", ")\n", "\n", "x = np.linspace(-2*np.pi, 2*np.pi, 100)\n", "def update_func():\n", " global x, code_input, cue_figure, parameter_panel \n", " y = code_ex.code(x, parameter_panel.parameters[\"omega\"])\n", " ax = cue_figure.figure.gca()\n", " ax.plot(x, y)\n", " \n", " \n", "code_ex = CodeExercise(\n", " code=code_input,\n", " parameters=parameter_panel,\n", " outputs=cue_figure,\n", " update=update_func,\n", ")\n", "\n", "\n", "code_ex.run_update() # For the demonstration we run the widget one time\n", "display(code_ex)" ] }, { "cell_type": "markdown", "id": "22", "metadata": {}, "source": [ "### ParametersPanel short constructors" ] }, { "cell_type": "markdown", "id": "23", "metadata": {}, "source": [ "The `ParametersPanel` can be also used with the same shorthand constructors as [interact](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html). Here are some examples." ] }, { "cell_type": "code", "execution_count": null, "id": "24", "metadata": { "tags": [] }, "outputs": [], "source": [ "from ipywidgets import fixed\n", "ParametersPanel(\n", " frequency=(0.5, 2*np.pi, 0.1),\n", " amplitude=(1, 5, 1),\n", " inverted=True,\n", " type=[\"sin\", \"cos\"],\n", " plot_title=\"trigonometric curve\",\n", " const=fixed(1) # this argument will be passed but is not changeable and therefore not displayed\n", ")" ] }, { "cell_type": "markdown", "id": "25", "metadata": {}, "source": [ "## Multiple choice exercise" ] }, { "cell_type": "code", "execution_count": null, "id": "26", "metadata": {}, "outputs": [], "source": [ "from scwidgets import MultipleChoiceExercise\n", "mcq_ex = MultipleChoiceExercise(\n", " options = [\"Hydrophobic surfaces\",\n", " \"Self-healing glass\",\n", " \"Transparent aluminum\",\n", " \"Conductive wood\",\n", " \"Spider silk paper\"],\n", " description = \"Which of the following are actual applications of materials science in use today?\",\n", " allow_multiple = False,\n", " randomize_order = True\n", ")\n", "display(mcq_ex)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.12.0" } }, "nbformat": 4, "nbformat_minor": 5 }