diff --git a/doc/sphinx/installation.rst b/doc/sphinx/installation.rst
index fc701b081de..3d1d44d0afc 100644
--- a/doc/sphinx/installation.rst
+++ b/doc/sphinx/installation.rst
@@ -89,7 +89,11 @@ are required:
.. code-block:: bash
sudo apt install python3-matplotlib python3-scipy ipython3 jupyter-notebook
- sudo pip3 install 'pint>=0.9'
+ pip3 install --user 'pint>=0.9' 'jupyter_contrib_nbextensions==0.5.1' \
+ 'sphinx>=1.6.7,!=2.1.0,!=3.0.0' 'sphinxcontrib-bibtex>=0.3.5'
+ jupyter contrib nbextension install --user
+ jupyter nbextension enable rubberband/main
+ jupyter nbextension enable exercise2/main
Nvidia GPU acceleration
"""""""""""""""""""""""
@@ -829,10 +833,10 @@ to actually write a simulation script for |es|.
Running an interactive notebook
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Running the Jupyter interpreter requires using the ``ipypresso`` script, which
+Running a Jupyter session requires using the ``ipypresso`` script, which
is also located in the build directory (its name comes from the IPython
interpreter, today known as Jupyter). To run the tutorials, you will need
-to start the Jupyter interpreter in notebook mode:
+to start a Jupyter session:
.. code-block:: bash
@@ -858,11 +862,16 @@ will exit the Python interpreter and Jupyter will notify you that the current
Python kernel stopped. If a cell takes too long to execute, you may interrupt
it with the stop button.
-To close the Jupyter notebook, go to the terminal where it was started and use
+Solutions cells are created using the ``exercise2`` plugin from nbextensions.
+To prevent solution code cells from running when clicking on "Run All", these
+code cells need to be converted to Markdown cells and fenced with `````python``
+and ```````.
+
+To close the Jupyter session, go to the terminal where it was started and use
the keyboard shortcut Ctrl+C twice.
-When starting the Jupyter interpreter in notebook mode, you may see the
-following warning in the terminal:
+When starting a Jupyter session, you may see the following warning in the
+terminal:
.. code-block:: none
diff --git a/doc/tutorials/04-lattice_boltzmann/04-lattice_boltzmann_part4.ipynb b/doc/tutorials/04-lattice_boltzmann/04-lattice_boltzmann_part4.ipynb
index 37eb1ace58e..460e79ac282 100644
--- a/doc/tutorials/04-lattice_boltzmann/04-lattice_boltzmann_part4.ipynb
+++ b/doc/tutorials/04-lattice_boltzmann/04-lattice_boltzmann_part4.ipynb
@@ -38,9 +38,7 @@
"\n",
"We will simulate a planar Poiseuille flow using a square box, two walls\n",
"with normal vectors $\\left(\\pm 1, 0, 0 \\right)$, and an external force density\n",
- "applied to every node.\n",
- "\n",
- "Use the data to fit a parabolic function. Can you confirm the analytic solution?"
+ "applied to every node."
]
},
{
@@ -52,6 +50,7 @@
"import logging\n",
"import sys\n",
"\n",
+ "import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"import espressomd\n",
@@ -102,9 +101,54 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "solution2": "hidden",
+ "solution2_first": true
+ },
"source": [
- "The solution is available at /doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution.py"
+ "Use the data to fit a parabolic function. Can you confirm the analytic solution?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "solution2": "hidden"
+ },
+ "source": [
+ "```python\n",
+ "# Extract fluid velocity along the x-axis\n",
+ "fluid_velocities = np.zeros((lbf.shape[0], 2))\n",
+ "for x in range(lbf.shape[0]):\n",
+ " # Average over the node in y direction\n",
+ " v_tmp = np.zeros(lbf.shape[1])\n",
+ " for y in range(lbf.shape[1]):\n",
+ " v_tmp[y] = lbf[x, y, 0].velocity[1]\n",
+ " fluid_velocities[x, 0] = (x + 0.5) * AGRID\n",
+ " fluid_velocities[x, 1] = np.average(v_tmp)\n",
+ "\n",
+ "def poiseuille_flow(x, force_density, dynamic_viscosity, height):\n",
+ " return force_density / (2.0 * dynamic_viscosity) * \\\n",
+ " (height**2.0 / 4.0 - x**2.0)\n",
+ "\n",
+ "# Note that the LB viscosity is not the dynamic viscosity but the\n",
+ "# kinematic viscosity (mu=LB_viscosity * density)\n",
+ "x_values = np.linspace(0.0, BOX_L, lbf.shape[0])\n",
+ "HEIGHT = BOX_L - 2.0 * AGRID\n",
+ "# analytical curve\n",
+ "y_values = poiseuille_flow(x_values - (HEIGHT / 2.0 + AGRID), FORCE_DENSITY[1],\n",
+ " VISCOSITY * DENSITY, HEIGHT)\n",
+ "# velocity is zero outside the walls\n",
+ "y_values[np.nonzero(x_values < WALL_OFFSET)] = 0.0\n",
+ "y_values[np.nonzero(x_values > BOX_L - WALL_OFFSET)] = 0.0\n",
+ "\n",
+ "fig1 = plt.figure(num=None, figsize=(10, 6), dpi=80, facecolor='w', edgecolor='k')\n",
+ "plt.plot(x_values, y_values, '-', linewidth=2, label='analytical')\n",
+ "plt.plot(fluid_velocities[:, 0], fluid_velocities[:, 1], 'o', label='simulation')\n",
+ "plt.xlabel('Position on the $x$-axis', fontsize=16)\n",
+ "plt.ylabel('Fluid velocity in $y$-direction', fontsize=16)\n",
+ "plt.legend()\n",
+ "plt.show()\n",
+ "```"
]
},
{
diff --git a/doc/tutorials/04-lattice_boltzmann/CMakeLists.txt b/doc/tutorials/04-lattice_boltzmann/CMakeLists.txt
index 56d99b48bd8..85f800c7f72 100644
--- a/doc/tutorials/04-lattice_boltzmann/CMakeLists.txt
+++ b/doc/tutorials/04-lattice_boltzmann/CMakeLists.txt
@@ -3,20 +3,7 @@ configure_tutorial_target(
04-lattice_boltzmann_part2.ipynb 04-lattice_boltzmann_part3.ipynb
04-lattice_boltzmann_part4.ipynb figures/latticeboltzmann-grid.png
figures/latticeboltzmann-momentumexchange.png
- scripts/04-lattice_boltzmann_part3_solution.py
- scripts/04-lattice_boltzmann_part4_solution.py)
-
-add_custom_command(
- OUTPUT
- "${CMAKE_CURRENT_BINARY_DIR}/scripts/04-lattice_boltzmann_part4_solution_cut.py"
- DEPENDS
- "${CMAKE_CURRENT_SOURCE_DIR}/scripts/04-lattice_boltzmann_part4_solution.py"
- DEPENDS
- "${CMAKE_CURRENT_SOURCE_DIR}/scripts/04-lattice_boltzmann_part4_solution_cut.cmake"
- COMMAND
- ${CMAKE_COMMAND} -P
- "${CMAKE_CURRENT_SOURCE_DIR}/scripts/04-lattice_boltzmann_part4_solution_cut.cmake"
-)
+ scripts/04-lattice_boltzmann_part3_solution.py)
nb_export(TARGET tutorial_04 SUFFIX "1" FILE "04-lattice_boltzmann_part1.ipynb"
HTML_RUN)
@@ -24,14 +11,5 @@ nb_export(TARGET tutorial_04 SUFFIX "2" FILE "04-lattice_boltzmann_part2.ipynb"
HTML_RUN)
nb_export(TARGET tutorial_04 SUFFIX "3" FILE "04-lattice_boltzmann_part3.ipynb"
HTML_RUN)
-nb_export(
- TARGET
- tutorial_04
- SUFFIX
- "4"
- FILE
- "04-lattice_boltzmann_part4.ipynb"
- HTML_RUN
- ADD_SCRIPTS
- "${CMAKE_CURRENT_BINARY_DIR}/scripts/04-lattice_boltzmann_part4_solution_cut.py"
-)
+nb_export(TARGET tutorial_04 SUFFIX "4" FILE "04-lattice_boltzmann_part4.ipynb"
+ HTML_RUN)
diff --git a/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution.py b/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution.py
deleted file mode 100644
index b2c946bec33..00000000000
--- a/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (C) 2019 The ESPResSo project
-#
-# This file is part of ESPResSo.
-#
-# ESPResSo is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ESPResSo is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-import matplotlib.pyplot as plt
-import numpy as np
-
-import espressomd
-espressomd.assert_features(['LB_BOUNDARIES_GPU'])
-import espressomd.lb
-import espressomd.lbboundaries
-import espressomd.shapes
-
-# System setup
-BOX_L = 16.0
-AGRID = 0.5
-VISCOSITY = 2.0
-FORCE_DENSITY = [0.0, 0.001, 0.0]
-DENSITY = 1.5
-TIME_STEP = 0.01
-system = espressomd.System(box_l=[BOX_L] * 3)
-system.time_step = TIME_STEP
-system.cell_system.skin = 0.4
-
-lbf = espressomd.lb.LBFluidGPU(
- agrid=AGRID, dens=DENSITY, visc=VISCOSITY, tau=TIME_STEP,
- ext_force_density=FORCE_DENSITY)
-system.actors.add(lbf)
-
-# Setup boundaries
-WALL_OFFSET = AGRID
-top_wall = espressomd.lbboundaries.LBBoundary(
- shape=espressomd.shapes.Wall(normal=[1, 0, 0], dist=WALL_OFFSET))
-bottom_wall = espressomd.lbboundaries.LBBoundary(
- shape=espressomd.shapes.Wall(normal=[-1, 0, 0], dist=-(BOX_L - WALL_OFFSET)))
-
-system.lbboundaries.add(top_wall)
-system.lbboundaries.add(bottom_wall)
-
-# Iterate until the flow profile converges (5000 LB updates)
-system.integrator.run(5000)
-
-# Extract fluid velocity along the x-axis
-fluid_velocities = np.zeros((lbf.shape[0], 2))
-for x in range(lbf.shape[0]):
- # Average over the node in y direction
- v_tmp = np.zeros(lbf.shape[1])
- for y in range(lbf.shape[1]):
- v_tmp[y] = lbf[x, y, 0].velocity[1]
- fluid_velocities[x, 0] = (x + 0.5) * AGRID
- fluid_velocities[x, 1] = np.average(v_tmp)
-
-
-def poiseuille_flow(x, force_density, dynamic_viscosity, height):
- return force_density / (2.0 * dynamic_viscosity) * \
- (height**2.0 / 4.0 - x**2.0)
-
-
-# Note that the LB viscosity is not the dynamic viscosity but the
-# kinematic viscosity (mu=LB_viscosity * density)
-x_values = np.linspace(0.0, BOX_L, lbf.shape[0])
-HEIGHT = BOX_L - 2.0 * AGRID
-# analytical curve
-y_values = poiseuille_flow(x_values - (HEIGHT / 2.0 + AGRID), FORCE_DENSITY[1],
- VISCOSITY * DENSITY, HEIGHT)
-# velocity is zero outside the walls
-y_values[np.nonzero(x_values < WALL_OFFSET)] = 0.0
-y_values[np.nonzero(x_values > BOX_L - WALL_OFFSET)] = 0.0
-
-plt.plot(x_values, y_values, 'o-', label='analytical')
-plt.plot(fluid_velocities[:, 0], fluid_velocities[:, 1], label='simulation')
-plt.legend()
-plt.show()
diff --git a/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution_cut.cmake b/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution_cut.cmake
deleted file mode 100644
index 3085c2e2a65..00000000000
--- a/doc/tutorials/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution_cut.cmake
+++ /dev/null
@@ -1,14 +0,0 @@
-# This code splits 04-lattice_boltzmann_part4_solution.py such that it can be
-# added to the tutorial without having two espressomd.System class instances
-file(
- READ
- "${CMAKE_CURRENT_SOURCE_DIR}/scripts/04-lattice_boltzmann_part4_solution.py"
- script_full)
-string(FIND "${script_full}" "# Extract fluid velocity along the x-axis"
- cut_position)
-string(SUBSTRING "${script_full}" ${cut_position} -1 script_cut)
-string(PREPEND script_cut "import matplotlib.pyplot as plt\n")
-file(
- WRITE
- "${CMAKE_CURRENT_BINARY_DIR}/scripts/04-lattice_boltzmann_part4_solution_cut.py"
- "${script_cut}")
diff --git a/doc/tutorials/CMakeLists.txt b/doc/tutorials/CMakeLists.txt
index 826cdf0d780..831b78ea6f2 100644
--- a/doc/tutorials/CMakeLists.txt
+++ b/doc/tutorials/CMakeLists.txt
@@ -53,29 +53,39 @@ function(NB_EXPORT)
set(PY_FILE "${NB_FILE_BASE}.py")
if(${NB_EXPORT_HTML_RUN})
- set(HTML_FILE_DEPENDS "${NB_FILE_BASE}.run${NB_FILE_EXT}")
+ set(NB_FILE_RUN "${NB_FILE_BASE}.run${NB_FILE_EXT}")
add_custom_command(
- OUTPUT "${HTML_FILE_DEPENDS}"
+ OUTPUT ${NB_FILE_RUN}
DEPENDS
"${NB_FILE};${NB_EXPORT_ADD_SCRIPTS};${CMAKE_BINARY_DIR}/doc/tutorials/html_runner.py;${CMAKE_BINARY_DIR}/testsuite/scripts/importlib_wrapper.py"
COMMAND
- "${CMAKE_BINARY_DIR}/pypresso"
- "${CMAKE_BINARY_DIR}/doc/tutorials/html_runner.py" "--input"
- "${NB_FILE}" "--output" "${HTML_FILE_DEPENDS}" "--substitutions"
- ${NB_EXPORT_VAR_SUBST} "--scripts" ${NB_EXPORT_ADD_SCRIPTS})
+ ${CMAKE_BINARY_DIR}/pypresso
+ ${CMAKE_BINARY_DIR}/doc/tutorials/html_runner.py --execute --exercise2
+ --input ${NB_FILE} --output ${NB_FILE_RUN} --substitutions
+ ${NB_EXPORT_VAR_SUBST} --scripts ${NB_EXPORT_ADD_SCRIPTS})
else()
- set(HTML_FILE_DEPENDS "${NB_FILE}")
+ set(NB_FILE_RUN ${NB_FILE})
endif()
add_custom_command(
- OUTPUT ${HTML_FILE} DEPENDS ${HTML_FILE_DEPENDS};${NB_EXPORT_ADD_SCRIPTS}
+ OUTPUT ${HTML_FILE}
+ DEPENDS ${NB_FILE_RUN};${NB_EXPORT_ADD_SCRIPTS}
+ COMMAND
+ ${CMAKE_BINARY_DIR}/pypresso
+ ${CMAKE_BINARY_DIR}/doc/tutorials/html_runner.py --exercise2 --input
+ ${NB_FILE_RUN} --output ${NB_FILE_RUN}~
COMMAND ${IPYTHON_EXECUTABLE} nbconvert --to "html" --output ${HTML_FILE}
- ${HTML_FILE_DEPENDS})
+ ${NB_FILE_RUN}~)
add_custom_command(
- OUTPUT ${PY_FILE} DEPENDS ${NB_FILE}
+ OUTPUT ${PY_FILE}
+ DEPENDS ${NB_FILE}
+ COMMAND
+ ${CMAKE_BINARY_DIR}/pypresso
+ ${CMAKE_BINARY_DIR}/doc/tutorials/html_runner.py --exercise2 --input
+ ${NB_FILE} --output ${NB_FILE}~
COMMAND ${IPYTHON_EXECUTABLE} nbconvert --to "python" --output ${PY_FILE}
- ${NB_FILE})
+ ${NB_FILE}~)
add_custom_target("${NB_EXPORT_TARGET}_html" DEPENDS ${HTML_FILE}
${DEPENDENCY_OF_TARGET})
diff --git a/doc/tutorials/html_runner.py b/doc/tutorials/html_runner.py
index 188b9e874de..f067d35e0ca 100644
--- a/doc/tutorials/html_runner.py
+++ b/doc/tutorials/html_runner.py
@@ -17,28 +17,33 @@
# along with this program. If not, see .
#
"""
-This script runs Jupyter notebooks and writes the output to new notebooks.
-Global variables can be edited to reduce runtime. External Python scripts
+This script processes Jupyter notebooks. External Python scripts
can be inserted as new code cells (e.g. solutions to exercises).
-The output notebooks can then be converted to HTML externally.
+Hidden solutions from the ``exercise2`` plugin can be converted
+to code cells. The notebook may also be executed, if necessary
+with modified global variables to reduce runtime. The processed
+notebook can then be converted to HTML externally.
"""
import argparse
parser = argparse.ArgumentParser(description='Process Jupyter notebooks.',
epilog=__doc__)
-parser.add_argument('--input', type=str,
+parser.add_argument('--input', type=str, nargs=1, required=True,
help='Path to the original Jupyter notebook')
-parser.add_argument('--output', type=str, nargs='?',
+parser.add_argument('--output', type=str, nargs=1,
help='Path to the processed Jupyter notebook')
parser.add_argument('--substitutions', nargs='*',
help='Variables to substitute')
parser.add_argument('--scripts', nargs='*',
help='Scripts to insert in new cells')
+parser.add_argument('--exercise2', action='store_true',
+ help='Convert exercise2 solutions into code cells')
+parser.add_argument('--execute', action='store_true',
+ help='Run the script')
args = parser.parse_args()
import nbformat
-from nbconvert.preprocessors import ExecutePreprocessor
import re
import os
import ast
@@ -60,22 +65,15 @@ def set_code_cells(nb, new_cells):
i += 1
-notebook_filepath = args.input
-notebook_filepath_edited = args.output or args.input + '~'
-notebook_dirname = os.path.dirname(notebook_filepath)
-new_values = args.substitutions or []
-new_cells = args.scripts or []
-
-# parse original notebook
-with open(notebook_filepath, encoding='utf-8') as f:
- nb = nbformat.read(f, as_version=4)
-
-# add new cells containing the solutions
-for filepath in new_cells:
+def add_cell_from_script(nb, filepath):
+ """
+ Create new code cell at the end of a notebook and populate it with
+ the content of a script.
+ """
with open(filepath, encoding='utf-8') as f:
code = f.read()
# remove ESPResSo copyright header
- m = re.search('# Copyright \(C\) \d+(?:-\d+)? The ESPResSo project\n.+?'
+ m = re.search('# Copyright \(C\) [\d\-,]+ The ESPResSo project\n.+?'
'If not, see \.\n', code, re.DOTALL)
if m and all(x.startswith('#') for x in m.group(0).strip().split('\n')):
code = re.sub('^(#\n)+', '', code.replace(m.group(0), ''), re.M)
@@ -91,57 +89,133 @@ def set_code_cells(nb, new_cells):
nb['cells'].append(cell_code)
+def disable_plot_interactivity(nb):
+ """
+ Replace all occurrences of the magic command ``%matplotlib notebook``
+ by ``%matplotlib inline``.
+ """
+ for cell in nb['cells']:
+ if cell['cell_type'] == 'code' and 'matplotlib' in cell['source']:
+ cell['source'] = re.sub('^%matplotlib +notebook',
+ '%matplotlib inline',
+ cell['source'], flags=re.M)
+
+
+def split_matplotlib_cells(nb):
+ """
+ If a cell imports matplotlib, split the cell to keep the
+ import statement separate from the code that uses matplotlib.
+ This prevents a known bug in the Jupyter backend which causes
+ the plot object to be represented as a string instead of a canvas
+ when created in the cell where matplotlib is imported for the
+ first time (https://github.com/jupyter/notebook/issues/3523).
+ """
+ for i in range(len(nb['cells']) - 1, -1, -1):
+ cell = nb['cells'][i]
+ if cell['cell_type'] == 'code' and 'matplotlib' in cell['source']:
+ code = iw.protect_ipython_magics(cell['source'])
+ # split cells after matplotlib imports
+ mapping = iw.delimit_statements(code)
+ tree = ast.parse(code)
+ visitor = iw.GetMatplotlibPyplot()
+ visitor.visit(tree)
+ if visitor.matplotlib_first:
+ code = iw.deprotect_ipython_magics(code)
+ lines = code.split('\n')
+ lineno_end = mapping[visitor.matplotlib_first]
+ split_code = '\n'.join(lines[lineno_end:]).lstrip('\n')
+ if split_code:
+ new_cell = nbformat.v4.new_code_cell(source=split_code)
+ nb['cells'].insert(i + 1, new_cell)
+ lines = lines[:lineno_end]
+ nb['cells'][i]['source'] = '\n'.join(lines).rstrip('\n')
+
+
+def convert_exercise2_to_code(nb):
+ """
+ Walk through the notebook cells and remove metadata associated with
+ the ``exercise2`` plugin from the contributed nbextensions. Solution
+ Markdown cells containing python code are converted to code cells.
+ """
+ for i in range(len(nb['cells']) - 1, 0, -1):
+ cell = nb['cells'][i]
+ cell_above = nb['cells'][i - 1]
+ # remove empty code cells after a solution cell
+ if cell['cell_type'] == 'code' and cell['source'].strip() == '' \
+ and 'solution2' in cell_above['metadata'] \
+ and 'solution2_first' not in cell_above['metadata'] \
+ and 'solution2' not in cell['metadata']:
+ nb['cells'].pop(i)
+ continue
+ # convert solution markdown cells into code cells
+ if cell['cell_type'] == 'markdown' and 'solution2' in cell['metadata'] \
+ and 'solution2_first' not in cell['metadata']:
+ lines = cell['source'].strip().split('\n')
+ if lines[0].startswith(
+ '```python') and lines[-1].startswith('```'):
+ source = '\n'.join(lines[1:-1]).strip()
+ nb['cells'][i] = nbformat.v4.new_code_cell(source=source)
+ # remove exercise2 metadata
+ for key in ('solution2', 'solution2_first'):
+ if key in cell['metadata']:
+ del cell['metadata'][key]
+
+
+def execute_notebook(nb, src, cell_separator):
+ """
+ Run the notebook in a python3 kernel. The ESPResSo visualizers are
+ disabled to prevent the kernel from crashing and to allow running
+ the notebook in a CI environment.
+ """
+ import nbconvert.preprocessors
+ notebook_dirname = os.path.dirname(notebook_filepath)
+ # disable OpenGL/Mayavi GUI
+ src_no_gui = iw.mock_es_visualization(src)
+ # update notebook with new code
+ set_code_cells(nb, src_no_gui.split(cell_separator))
+ # execute notebook
+ ep = nbconvert.preprocessors.ExecutePreprocessor(
+ timeout=20 * 60, kernel_name='python3')
+ ep.preprocess(nb, {'metadata': {'path': notebook_dirname}})
+ # restore notebook with code before the GUI removal step
+ set_code_cells(nb, src.split(cell_separator))
+
+
+notebook_filepath = args.input[0]
+if args.output:
+ notebook_filepath_edited = args.output[0]
+else:
+ notebook_filepath_edited = notebook_filepath + '~'
+
+# parse original notebook
+with open(notebook_filepath, encoding='utf-8') as f:
+ nb = nbformat.read(f, as_version=4)
+
+# add new cells containing the solutions
+if args.scripts:
+ for filepath in args.scripts:
+ add_cell_from_script(nb, filepath)
+
# disable plot interactivity
-for i in range(len(nb['cells'])):
- cell = nb['cells'][i]
- if cell['cell_type'] == 'code' and 'matplotlib' in cell['source']:
- cell['source'] = re.sub('^%matplotlib +notebook', '%matplotlib inline',
- cell['source'], flags=re.M)
-
-
-# if matplotlib is used in this script, split cell to keep the import
-# statement separate and avoid a known bug in the Jupyter backend which
-# causes the plot object to be represented as a string instead of a
-# canvas when created in the cell where matplotlib is imported for the
-# first time (https://github.com/jupyter/notebook/issues/3523)
-for i in range(len(nb['cells'])):
- cell = nb['cells'][i]
- if cell['cell_type'] == 'code' and 'matplotlib' in cell['source']:
- code = iw.protect_ipython_magics(cell['source'])
- # split cells after matplotlib imports
- mapping = iw.delimit_statements(code)
- tree = ast.parse(code)
- visitor = iw.GetMatplotlibPyplot()
- visitor.visit(tree)
- if visitor.matplotlib_first:
- code = iw.deprotect_ipython_magics(code)
- lines = code.split('\n')
- lineno_end = mapping[visitor.matplotlib_first]
- split_code = '\n'.join(lines[lineno_end:]).lstrip('\n')
- if split_code:
- new_cell = nbformat.v4.new_code_cell(source=split_code)
- nb['cells'].insert(i + 1, new_cell)
- lines = lines[:lineno_end]
- nb['cells'][i]['source'] = '\n'.join(lines).rstrip('\n')
- break
-
-# substitute global variables and disable OpenGL/Mayavi GUI
-cell_separator = '\n##{}\n'.format(uuid.uuid4().hex)
-src = cell_separator.join(get_code_cells(nb))
-parameters = dict(x.split('=', 1) for x in new_values)
-src = iw.substitute_variable_values(src, strings_as_is=True,
- keep_original=False, **parameters)
-src_no_gui = iw.mock_es_visualization(src)
-
-# update notebook with new code
-set_code_cells(nb, src_no_gui.split(cell_separator))
-
-# execute notebook
-ep = ExecutePreprocessor(timeout=20 * 60, kernel_name='python3')
-ep.preprocess(nb, {'metadata': {'path': notebook_dirname}})
-
-# restore notebook with code before the GUI removal step
-set_code_cells(nb, src.split(cell_separator))
+disable_plot_interactivity(nb)
+
+# guard against a jupyter bug involving matplotlib
+split_matplotlib_cells(nb)
+
+if args.exercise2:
+ convert_exercise2_to_code(nb)
+
+if args.substitutions or args.execute:
+ # substitute global variables
+ cell_separator = '\n##{}\n'.format(uuid.uuid4().hex)
+ src = cell_separator.join(get_code_cells(nb))
+ new_values = args.substitutions or []
+ parameters = dict(x.split('=', 1) for x in new_values)
+ src = iw.substitute_variable_values(src, strings_as_is=True,
+ keep_original=False, **parameters)
+ set_code_cells(nb, src.split(cell_separator))
+ if args.execute:
+ execute_notebook(nb, src, cell_separator)
# write edited notebook
with open(notebook_filepath_edited, 'w', encoding='utf-8') as f:
diff --git a/requirements.txt b/requirements.txt
index 5fd6d5c3a11..86d78b471d9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,6 +20,8 @@ lxml>=4.2.1 # to deploy tutorials
# sphinx and its dependencies
sphinx>=1.6.7,!=2.1.0,!=3.0.0
sphinxcontrib-bibtex>=0.3.5
+# jupyter dependencies
+jupyter_contrib_nbextensions==0.5.1
# pep8 and its dependencies
autopep8==1.3.4
pycodestyle==2.3.1
diff --git a/testsuite/scripts/tutorials/CMakeLists.txt b/testsuite/scripts/tutorials/CMakeLists.txt
index 3977e95e287..0469cbab9ff 100644
--- a/testsuite/scripts/tutorials/CMakeLists.txt
+++ b/testsuite/scripts/tutorials/CMakeLists.txt
@@ -37,7 +37,6 @@ tutorial_test(FILE test_04-lattice_boltzmann_part2.py LABELS "gpu")
tutorial_test(FILE test_04-lattice_boltzmann_part3.py LABELS "gpu")
tutorial_test(FILE test_04-lattice_boltzmann_part3_solution.py LABELS "gpu")
tutorial_test(FILE test_04-lattice_boltzmann_part4.py LABELS "gpu")
-tutorial_test(FILE test_04-lattice_boltzmann_part4_solution.py LABELS "gpu")
tutorial_test(FILE test_05-raspberry_electrophoresis.py LABELS "gpu")
tutorial_test(FILE test_06-active_matter__flow_field.py LABELS "gpu")
tutorial_test(FILE test_06-active_matter__rectification_geometry.py)
diff --git a/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4.py b/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4.py
index aaccf3b6dc2..58dded8160c 100644
--- a/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4.py
+++ b/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4.py
@@ -17,6 +17,7 @@
import unittest as ut
import importlib_wrapper
+import numpy as np
tutorial, skipIfMissingFeatures = importlib_wrapper.configure_and_import(
@@ -28,6 +29,12 @@
class Tutorial(ut.TestCase):
system = tutorial.system
+ def test_flow_profile(self):
+ analytical = tutorial.y_values
+ simulation = tutorial.fluid_velocities[:, 1]
+ rmsd = np.sqrt(np.mean(np.square(analytical - simulation)))
+ self.assertLess(rmsd, 2e-5 * tutorial.AGRID / tutorial.lbf.tau)
+
if __name__ == "__main__":
ut.main()
diff --git a/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4_solution.py b/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4_solution.py
deleted file mode 100644
index dc8eec29e69..00000000000
--- a/testsuite/scripts/tutorials/test_04-lattice_boltzmann_part4_solution.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (C) 2019 The ESPResSo project
-#
-# This file is part of ESPResSo.
-#
-# ESPResSo is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ESPResSo is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-import unittest as ut
-import importlib_wrapper
-import numpy as np
-
-
-tutorial, skipIfMissingFeatures = importlib_wrapper.configure_and_import(
- "@TUTORIALS_DIR@/04-lattice_boltzmann/scripts/04-lattice_boltzmann_part4_solution.py",
- gpu=True)
-
-
-@skipIfMissingFeatures
-class Tutorial(ut.TestCase):
- def test_flow_profile(self):
- analytical = tutorial.y_values
- simulation = tutorial.fluid_velocities[:, 1]
- rmsd = np.sqrt(np.mean(np.square(analytical - simulation)))
- self.assertLess(rmsd, 2e-5 * tutorial.AGRID / tutorial.lbf.tau)
-
-
-if __name__ == "__main__":
- ut.main()
diff --git a/testsuite/scripts/tutorials/test_html_runner.py b/testsuite/scripts/tutorials/test_html_runner.py
index 049f421ba9f..89b625046cb 100644
--- a/testsuite/scripts/tutorials/test_html_runner.py
+++ b/testsuite/scripts/tutorials/test_html_runner.py
@@ -91,7 +91,9 @@ def test_html_wrapper(self):
'--input', f_input,
'--output', f_output,
'--scripts', f_script,
- '--substitutions', 'global_var=20']
+ '--substitutions', 'global_var=20',
+ '--execute']
+ print('Running command ' + ' '.join(cmd))
completedProc = subprocess.run(cmd)
# check the command ran without any error
self.assertEqual(completedProc.returncode, 0, 'non-zero return code')
@@ -128,6 +130,77 @@ def test_html_wrapper(self):
self.assertEqual(nb_output['cells'][4]['cell_type'], 'code')
self.assertEqual(nb_output['cells'][4]['source'], 'global_var = 20')
+ def test_exercise2_plugin(self):
+ f_input = '@CMAKE_CURRENT_BINARY_DIR@/test_html_runner_exercise2.ipynb'
+ f_output = '@CMAKE_CURRENT_BINARY_DIR@/test_html_runner_exercise2.run.ipynb'
+ # setup
+ if os.path.isfile(f_output):
+ os.remove(f_output)
+ with open(f_input, 'w', encoding='utf-8') as f:
+ nb = nbformat.v4.new_notebook(metadata=self.nb_metadata)
+ # question with 2 answers and an empty cell
+ cell_md = nbformat.v4.new_markdown_cell(source='Question 1')
+ cell_md['metadata']['solution2_first'] = True
+ cell_md['metadata']['solution2'] = 'shown'
+ nb['cells'].append(cell_md)
+ code = '```python\n1\n```'
+ cell_md = nbformat.v4.new_markdown_cell(source=code)
+ cell_md['metadata']['solution2'] = 'shown'
+ nb['cells'].append(cell_md)
+ cell_md = nbformat.v4.new_markdown_cell(source='1b')
+ cell_md['metadata']['solution2'] = 'shown'
+ nb['cells'].append(cell_md)
+ cell_code = nbformat.v4.new_code_cell(source='')
+ nb['cells'].append(cell_code)
+ # question with 1 answer and a non-empty cell
+ cell_md = nbformat.v4.new_markdown_cell(source='Question 2')
+ cell_md['metadata']['solution2_first'] = True
+ cell_md['metadata']['solution2'] = 'hidden'
+ nb['cells'].append(cell_md)
+ code = '```python\n2\nglobal_var = 5\n```'
+ cell_md = nbformat.v4.new_markdown_cell(source=code)
+ cell_md['metadata']['solution2'] = 'hidden'
+ nb['cells'].append(cell_md)
+ cell_code = nbformat.v4.new_code_cell(source='3')
+ nb['cells'].append(cell_code)
+ nbformat.write(nb, f)
+ # run command
+ cmd = ['@CMAKE_BINARY_DIR@/pypresso',
+ '@CMAKE_BINARY_DIR@/doc/tutorials/html_runner.py',
+ '--input', f_input,
+ '--output', f_output,
+ '--substitutions', 'global_var=20',
+ '--exercise2']
+ print('Running command ' + ' '.join(cmd))
+ completedProc = subprocess.run(cmd)
+ # check the command ran without any error
+ self.assertEqual(completedProc.returncode, 0, 'non-zero return code')
+ self.assertTrue(os.path.isfile(f_output), f_output + ' not created')
+ # read processed notebook
+ with open(f_output, encoding='utf-8') as f:
+ nb_output = nbformat.read(f, as_version=4)
+ # check cells
+ cells = iter(nb_output['cells'])
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'markdown')
+ self.assertEqual(cell['source'], 'Question 1')
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'code')
+ self.assertEqual(cell['source'], '1')
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'markdown')
+ self.assertEqual(cell['source'], '1b')
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'markdown')
+ self.assertEqual(cell['source'], 'Question 2')
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'code')
+ self.assertEqual(cell['source'], '2\nglobal_var = 20')
+ cell = next(cells)
+ self.assertEqual(cell['cell_type'], 'code')
+ self.assertEqual(cell['source'], '3')
+ self.assertEqual(next(cells, 'EOF'), 'EOF')
+
if __name__ == "__main__":
ut.main()