Important

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.

Writing exercises

scicode-widgets provides a flexible code widget that allows instant feedback to evaluate the code for interactive plots.

[1]:
from scwidgets import (
    CodeExercise,
    CodeInput,
    CueFigure,
    CueOutput,
    CueObject,
    ExerciseRegistry,
    ParametersPanel,
    TextExercise,
)

from ipywidgets import FloatSlider, IntSlider
import matplotlib.pyplot as plt
import numpy as np

ExerciseRegistry to store and load answers to exercises

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.

[2]:
exercise_registry = ExerciseRegistry()
exercise_registry
[2]:

TextExercise to create text exercises

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.

[3]:
TextExercise(
    title="Exercise 01: Derive a solution for weights",
    description="""
    We can define ridge regression by extending the ordinary least
    square solution by a penalization $\lambda\|\mathbf{w}\|_2^2$. Please derive the solution
    for the weights from the optimization problem:</p>
    $$\hat{\mathbf{w}} = \min_\mathbf{w} \|\mathbf{y}-\mathbf{X}\mathbf{w}\|^2 + \lambda\|\mathbf{w}\|^2$$""",
    key="ex01", # the key it is stored under in the json file
    exercise_registry=exercise_registry
)
[3]:

Interactive coding exercises

This is an example on how to create a simple exercise.

[4]:
# This is what the students sees and can adapt
def sin(x: int, omega=1.0):
    """
    Implements ridge regression

    :param x: An array of data points
    :param omega: The frequency
    """
    import numpy as np
    return np.sin(x*omega)

def update_func(code_ex):
    x = np.linspace(-2*np.pi, 2*np.pi, 100)
    y = code_ex.code(x, code_ex.parameters["omega"])
    ax = code_ex.figure.gca()
    ax.plot(x, y)

code_ex_description = """
Implements a sinus function $\sin(x\omega)$.
"""
code_ex = CodeExercise(
    code=sin,
    parameters={'omega': (0.5, 3.14, 0.1)},
    outputs=plt.figure(),
    update=update_func,
    update_mode="continuous", # we also support ["manual", "release"]
    title="Sinus function",
    description=code_ex_description,
    key="sin_local",
    exercise_registry=exercise_registry
)

code_ex.run_update() # For the demonstration we run the widget one time
display(code_ex)

Creating widget components beforehand

More complex widgets might need to be created beforehand to allow full customization

[5]:
from ipywidgets import HTML

# One can pass a function also by
code_input = CodeInput(
    function_name="sin",
    function_parameters="x: int, omega=1.0",
    function_body="import numpy as np\nreturn np.sin(x*omega)",
    docstring="Implements ridge regression\n\n:param x: An array of data points\n:param omega: The frequency"
)
# customization of figure toolbar, only important in widget mode (use %matplotlib widget)
figure = CueFigure(plt.figure(), show_toolbars=True)
# to use a custom output for own widgets
output = CueOutput()
# to use display custom widgets
table = CueObject(HTML(value="<table><tr><th>x</th><th>y</th></tr></table>"))


# to customize sliders, one can directy
parameter_panel = ParametersPanel(
    omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description="$\\omega$")
)
# alternatively if passed to CodeExercise this also works
#parameter_panel = dict(
#    omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description="$\\omega$")
#)


def update_func(code_ex):
    x = np.linspace(-2*np.pi, 2*np.pi, 100)
    y = code_ex.code(x, code_ex.parameters["omega"])
    ax = code_ex.outputs[0].figure.gca()
    ax.plot(x, y)
    with code_ex.outputs[1]:
        print("Some text after the figure")

    code_ex.outputs[2].object.value = "<table style=\"width:50%\"><tr><th>x</th><th>y</th></tr>" + \
         "".join([f"<tr><td>{x[i]:.2f}</td><td>{y[i]:.2f}</td></tr>" for i in range(0, len(x), 20)]) + \
         "</table>"
    # the captured text in the function is always printed before any other output
    print("Some text before the figure")



code_ex = CodeExercise(
    code=code_input,
    parameters=parameter_panel,
    outputs=[figure, output, table],
    update=update_func,
)

code_ex.run_update() # For the demonstration we run the widget one time
display(code_ex)

Include imports to code input

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.

[6]:
def sin(x: np.ndarray, omega=1.0): # using np.ndarray requires import numpy before the function body
    """
    Implements ridge regression

    :param x: An array of data points
    :param omega: The frequency
    """
    return np.sin(x*omega)

def update_func(code_ex):
    x = np.linspace(-2*np.pi, 2*np.pi, 100)
    y = code_ex.code(x, code_ex.parameters["omega"])
    ax = code_ex.figure.gca()
    ax.plot(x, y)

code_ex = CodeExercise(
    code=CodeInput(sin, builtins={'np': np}),
    parameters={'omega': (0.5, 3.14, 0.1)},
    outputs=plt.figure(),
    update=update_func,
)

code_ex.run_update() # For the demonstration we run the widget one time
display(code_ex)

Interactive coding exercises with globals variables

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.

[7]:
# This is what the students sees and can adapt
def sin(x, omega):
    """
    Implements ridge regression

    :param x: An array of data points
    :param omega: The frequency
    """
    import numpy as np
    return np.sin(x*omega)

code_input = CodeInput(sin)
cue_figure = CueFigure(plt.figure())
parameter_panel = ParametersPanel(
    omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description="$\\omega$")
)

x = np.linspace(-2*np.pi, 2*np.pi, 100)
def update_func():
    global x, code_input, cue_figure, parameter_panel
    y = code_ex.code(x, parameter_panel.parameters["omega"])
    ax = cue_figure.figure.gca()
    ax.plot(x, y)


code_ex = CodeExercise(
    code=code_input,
    parameters=parameter_panel,
    outputs=cue_figure,
    update=update_func,
)


code_ex.run_update() # For the demonstration we run the widget one time
display(code_ex)

ParametersPanel short constructors

The ParametersPanel can be also used with the same shorthand constructors as interact. Here are some examples.

[8]:
from ipywidgets import fixed
ParametersPanel(
    frequency=(0.5, 2*np.pi, 0.1),
    amplitude=(1, 5, 1),
    inverted=True,
    type=["sin", "cos"],
    plot_title="trigonometric curve",
    const=fixed(1) # this argument will be passed but is not changeable and therefore not displayed
)
[8]:

Multiple choice exercise

[9]:
from scwidgets import MultipleChoiceExercise
mcq_ex = MultipleChoiceExercise(
    options = ["Hydrophobic surfaces",
               "Self-healing glass",
               "Transparent aluminum",
               "Conductive wood",
               "Spider silk paper"],
    description = "Which of the following are actual applications of materials science in use today?",
    allow_multiple = False,
    randomize_order = True
)
display(mcq_ex)