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()