diff --git a/ProcessLib/SmallDeformation/Tests.cmake b/ProcessLib/SmallDeformation/Tests.cmake
index 704d03ba4abcd8d6a29ac9e87d4bf97d304ec2de..1787f24f1e42cd063841776e3d7a8858c2ab471a 100644
--- a/ProcessLib/SmallDeformation/Tests.cmake
+++ b/ProcessLib/SmallDeformation/Tests.cmake
@@ -316,3 +316,20 @@ AddTest(
     ref_piston_pcs_0_ts_10_t_10.000000.vtu piston_pcs_0_ts_10_t_10.000000.vtu epsilon epsilon 1e-13 0
     ref_piston_pcs_0_ts_10_t_10.000000.vtu piston_pcs_0_ts_10_t_10.000000.vtu sigma sigma 1e-7 0
 )
+
+AddTest(
+    NAME LARGE_PythonBCSmallDeformationHertzContact
+    PATH Mechanics/Linear/PythonHertzContact
+    EXECUTABLE ogs
+    EXECUTABLE_ARGS hertz_contact.prj
+    WRAPPER time
+    TESTER vtkdiff
+    REQUIREMENTS OGS_USE_PYTHON AND NOT OGS_USE_MPI
+    DIFF_DATA
+    ref_hertz_contact_ts_5.vtu  hertz_pcs_0_ts_5_t_5.000000.vtu  displacement displacement 1e-16 0
+    ref_hertz_contact_ts_5.vtu  hertz_pcs_0_ts_5_t_5.000000.vtu  epsilon epsilon 1e-16 0
+    ref_hertz_contact_ts_5.vtu  hertz_pcs_0_ts_5_t_5.000000.vtu  sigma sigma 1e-16 0
+    ref_hertz_contact_ts_10.vtu hertz_pcs_0_ts_10_t_10.000000.vtu displacement displacement 1e-16 0
+    ref_hertz_contact_ts_10.vtu hertz_pcs_0_ts_10_t_10.000000.vtu epsilon epsilon 1e-16 0
+    ref_hertz_contact_ts_10.vtu hertz_pcs_0_ts_10_t_10.000000.vtu sigma sigma 1e-16 0
+)
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/gen-unit-circle.py b/Tests/Data/Mechanics/Linear/PythonHertzContact/gen-unit-circle.py
new file mode 100755
index 0000000000000000000000000000000000000000..e7a4d0110886d4d7b6b3e0338146acfcdb0cadd0
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/gen-unit-circle.py
@@ -0,0 +1,126 @@
+#!/usr/bin/vtkpython
+
+import sys
+import numpy as np
+from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk
+import vtk
+
+in_grid, out_grid, out_geom = sys.argv[1:]
+
+
+def distribute_points_evenly(c2):
+    assert np.sqrt(c2.shape[0]).is_integer()
+
+    CELLS_PER_DIRECTION = int(np.sqrt(c2.shape[0])) - 1
+    r2 = np.sqrt(c2[:,0]**2 + c2[:,1]**2)
+    alpha2 = np.arctan2(c2[:,1], c2[:,0])
+
+    bins = [ [] for _ in range(CELLS_PER_DIRECTION+1) ]
+    nbin = np.round(r2 * CELLS_PER_DIRECTION).astype(int)
+    for node, b in enumerate(nbin):
+        bins[b].append(node)
+
+    for b in bins:
+        b.sort(key=lambda n: alpha2[n])
+        # print(len(b))
+
+    c3 = np.zeros_like(c2)
+
+    for node, r in enumerate(r2):
+        b = nbin[node]
+        i = bins[b].index(node)
+        if len(bins[b]) == 1:
+            phi = 0.0
+        else:
+            phi = np.pi * 0.5 / (len(bins[b]) - 1) * i
+
+        c3[node, 0] = r * np.cos(phi)
+        c3[node, 1] = r * np.sin(phi)
+
+    return c3
+
+
+reader = vtk.vtkXMLUnstructuredGridReader()
+reader.SetFileName(in_grid)
+reader.Update()
+
+grid = reader.GetOutput()
+assert grid.GetBounds() == (0.0, 1.0, 0.0, 1.0, 0.0, 0.0)
+
+coords = vtk_to_numpy(grid.GetPoints().GetData())
+
+a = np.empty(coords.shape[0])
+
+for i, (x, y, z) in enumerate(coords):
+    if x > y:
+        R = np.sqrt(1 + (y/x)**2)
+        a[i] = 1.0 / R
+    elif x < y:
+        R = np.sqrt(1 + (x/y)**2)
+        a[i] = 1.0 / R
+    else:
+        a[i] = np.sqrt(0.5)
+
+# scale coordinates
+new_coords = np.multiply(coords.T, a).T
+
+if False:
+    # If the input is a regular mesh, there is the possibility to make bins of
+    # points with equal radius. For each radius those points can than be
+    # distributed with even spacing at the quarter circles.
+    new_coords = distribute_points_evenly(new_coords)
+
+new_coords_vtk = numpy_to_vtk(new_coords, 1)
+pts = vtk.vtkPoints()
+pts.SetData(new_coords_vtk)
+grid.SetPoints(pts)
+
+writer = vtk.vtkXMLUnstructuredGridWriter()
+writer.SetFileName(out_grid)
+writer.SetInputData(grid)
+writer.Write()
+
+# extract boundary of unit circle
+R_squared = new_coords[:,0]**2 + new_coords[:,1]**2
+phi = np.arctan2(new_coords[:,1], new_coords[:,0])
+
+idcs = np.where(abs(R_squared - 1) < 1e-8)[0]
+idcs = idcs[np.argsort(phi[idcs])]  # sorted with ascending polar angle
+
+
+with open(out_geom, "w") as fh:
+    fh.write("""<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="OpenGeoSysGLI.xsl"?>
+
+<OpenGeoSysGLI xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.opengeosys.org/images/xsd/OpenGeoSysGLI.xsd" xmlns:ogs="http://www.opengeosys.org">
+    <name>geom</name>
+    <points>
+        <point id="0" x="0" y="0" z="0" name="center"/>
+        <point id="1" x="0" y="1" z="0" name="top"/>
+        <point id="2" x="1" y="0" z="0"/>
+""")
+
+    for i, (x, y, z) in enumerate(new_coords[idcs]):
+        fh.write('        <point id="{}" x="{}" y="{}" z="{}" />\n'.format(
+            i+3, x, y, z))
+
+    fh.write("""
+    </points>
+
+    <polylines>
+        <polyline id="0" name="left">
+            <pnt>0</pnt>
+            <pnt>1</pnt>
+        </polyline>
+        <polyline id="1" name="bottom">
+            <pnt>0</pnt>
+            <pnt>2</pnt>
+        </polyline>
+        <polyline id="2" name="outer">\n""")
+
+    for i in range(len(idcs)):
+        fh.write("            <pnt>{}</pnt>\n".format(i+3))
+
+    fh.write("""</polyline>
+    </polylines>
+</OpenGeoSysGLI>""")
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact.prj b/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact.prj
new file mode 100644
index 0000000000000000000000000000000000000000..8cbeeb41991bf27ac1c70922025bff33b701ec23
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact.prj
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<OpenGeoSysProject>
+    <mesh axially_symmetric="true">unit-circle.vtu</mesh>
+    <geometry>unit-circle.gml</geometry>
+    <python_script>hertz_contact_bc.py</python_script>
+    <processes>
+        <process>
+            <name>SD</name>
+            <type>SMALL_DEFORMATION</type>
+            <integration_order>2</integration_order>
+            <constitutive_relation>
+                <type>LinearElasticIsotropic</type>
+                <youngs_modulus>E</youngs_modulus>
+                <poissons_ratio>nu</poissons_ratio>
+            </constitutive_relation>
+            <solid_density>rho_sr</solid_density>
+            <specific_body_force>0 0</specific_body_force>
+            <process_variables>
+                <process_variable>displacement</process_variable>
+            </process_variables>
+            <secondary_variables>
+                <secondary_variable type="static" internal_name="sigma" output_name="sigma"/>
+                <secondary_variable type="static" internal_name="epsilon" output_name="epsilon"/>
+            </secondary_variables>
+        </process>
+    </processes>
+    <time_loop>
+        <processes>
+            <process ref="SD">
+                <nonlinear_solver>basic_newton</nonlinear_solver>
+                <convergence_criterion>
+                    <type>DeltaX</type>
+                    <norm_type>INFINITY_N</norm_type>
+                    <abstol>1e-10</abstol>
+                </convergence_criterion>
+                <time_discretization>
+                    <type>BackwardEuler</type>
+                </time_discretization>
+                <output>
+                    <variables>
+                        <variable>displacement</variable>
+                        <variable>sigma</variable>
+                        <variable>epsilon</variable>
+                    </variables>
+                </output>
+                <time_stepping>
+                    <type>FixedTimeStepping</type>
+                    <t_initial>0</t_initial>
+                    <t_end>10</t_end>
+                    <timesteps>
+                        <pair>
+                            <repeat>1</repeat>
+                            <delta_t>1</delta_t>
+                        </pair>
+                    </timesteps>
+                </time_stepping>
+            </process>
+        </processes>
+        <output>
+            <type>VTK</type>
+            <prefix>hertz</prefix>
+            <timesteps>
+                <pair>
+                    <repeat>1</repeat>
+                    <each_steps>1</each_steps>
+                </pair>
+            </timesteps>
+            <output_iteration_results>false</output_iteration_results>
+        </output>
+    </time_loop>
+    <parameters>
+        <parameter>
+            <name>E</name>
+            <type>Constant</type>
+            <value>1</value>
+        </parameter>
+        <parameter>
+            <name>nu</name>
+            <type>Constant</type>
+            <value>.3</value>
+        </parameter>
+        <parameter>
+            <name>rho_sr</name>
+            <type>Constant</type>
+            <value>1</value>
+        </parameter>
+        <parameter>
+            <name>displacement0</name>
+            <type>Constant</type>
+            <values>0 0</values>
+        </parameter>
+        <parameter>
+            <name>zero</name>
+            <type>Constant</type>
+            <value>0</value>
+        </parameter>
+    </parameters>
+    <process_variables>
+        <process_variable>
+            <name>displacement</name>
+            <components>2</components>
+            <order>1</order>
+            <initial_condition>displacement0</initial_condition>
+            <boundary_conditions>
+                <boundary_condition>
+                    <geometrical_set>geom</geometrical_set>
+                    <geometry>left</geometry>
+                    <type>Dirichlet</type>
+                    <component>0</component>
+                    <parameter>zero</parameter>
+                </boundary_condition>
+                <boundary_condition>
+                    <geometrical_set>geom</geometrical_set>
+                    <geometry>bottom</geometry>
+                    <type>Dirichlet</type>
+                    <component>1</component>
+                    <parameter>zero</parameter>
+                </boundary_condition>
+                <boundary_condition>
+                    <geometrical_set>geom</geometrical_set>
+                    <geometry>outer</geometry>
+                    <type>Python</type>
+                    <component>1</component>
+                    <bc_object>bc</bc_object>
+                    <flush_stdout>false</flush_stdout> <!-- for debugging only -->
+                </boundary_condition>
+            </boundary_conditions>
+        </process_variable>
+    </process_variables>
+    <nonlinear_solvers>
+        <nonlinear_solver>
+            <name>basic_newton</name>
+            <type>Newton</type>
+            <max_iter>50</max_iter>
+            <linear_solver>general_linear_solver</linear_solver>
+        </nonlinear_solver>
+    </nonlinear_solvers>
+    <linear_solvers>
+        <linear_solver>
+            <name>general_linear_solver</name>
+            <lis>-i cg -p jacobi -tol 1e-16 -maxiter 10000</lis>
+            <eigen>
+                <solver_type>CG</solver_type>
+                <precon_type>DIAGONAL</precon_type>
+                <max_iteration_step>10000</max_iteration_step>
+                <error_tolerance>1e-16</error_tolerance>
+            </eigen>
+            <petsc>
+                <prefix>sd</prefix>
+                <parameters>-sd_ksp_type cg -sd_pc_type bjacobi -sd_ksp_rtol 1e-16 -sd_ksp_max_it 10000</parameters>
+            </petsc>
+        </linear_solver>
+    </linear_solvers>
+</OpenGeoSysProject>
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact_bc.py b/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact_bc.py
new file mode 100644
index 0000000000000000000000000000000000000000..5351190b431c9cac449abd5c8b1600783368e83b
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/hertz_contact_bc.py
@@ -0,0 +1,111 @@
+from __future__ import print_function
+
+import OpenGeoSys
+
+
+# enable/disable debug output
+if True:
+    def debug(*args):
+        print(*args)
+else:
+    def debug(*args):
+        pass
+
+
+SPHERE_RADIUS = 1.0
+START_TIME = 0.0
+
+
+class HertzContactBC(OpenGeoSys.BoundaryCondition):
+    def __init__(self):
+        super(HertzContactBC, self).__init__()
+
+        self._first_node = None         # ID of the first node of this BC's geometry
+        self._t_old = START_TIME - 1.0  # time of previous invocation of this BC
+
+
+    def _init_timestep(self):
+        """Initializes the internal state at the beginning of a new timestep."""
+
+        self._a_range = [ 0.0, SPHERE_RADIUS ]  # range of possible contact radii
+        self._a_est = SPHERE_RADIUS             # estimated contact radius
+
+        self._max_x_with_y_excess = -1.0        # maximum x value where a node is above the contact line
+
+
+    def _init_iteration(self):
+        """Initializes the internal state at the beginning of a new nonlinear solver iteration."""
+
+        # variant of a bisection algorithm
+        if self._max_x_with_y_excess >= 0.0:
+            # there were nodes above the contact area in the previous
+            # iteration => contact area has to grow.
+            if self._max_x_with_y_excess < self._a_est:
+                # This is an ad-hoc variation of the bisection algorithm for
+                # finding the radius of the contact area. The variation is
+                # necessary, because we obtain the value of
+                # self._max_x_with_y_excess only after we already applied the
+                # BCs for the current nonlinear solver iteration, i.e., the
+                # applied BCs and the information based on which the BCs are
+                # applied are off by one iteration.
+                self._a_range[1] = self._max_x_with_y_excess
+                assert self._a_range[0] <= self._a_range[1]
+            else:
+                self._a_range[0] = self._a_est
+        else:
+            # contact area has to shrink
+            self._a_range[1] = self._a_est
+
+        self._a_est = 0.5 * sum(self._a_range)
+        print("BC: a_est={:.4f}, ({:.4f}, {:.4f})".format(self._a_est, self._a_range[0], self._a_range[1]))
+
+        self._max_x_with_y_excess = -1.0
+
+
+    def getDirichletBCValue(self, t, coords, node_id, primary_vars):
+        if self._t_old < t:
+            self._t_old = t
+            self._init_timestep()
+
+        # detect nonlinear iteration, assumes that nodes are always processed in
+        # the same order
+        if self._first_node is None:
+            self._first_node = node_id
+
+        if self._first_node == node_id:
+            self._init_iteration()
+
+        x, y, z = coords
+        ux, uy = primary_vars
+
+        try:
+            # check that we are at the outer boundary
+            assert abs(x**2 + y**2 + z**2 - 1.0) < 1e-15
+        except:
+            print("assert abs(x**2 + y**2 + z**2 - 1.0) < 1e-15",
+                    x, y, z, x**2 + y**2 + z**2 - 1)
+            raise
+
+        y_deformed = y + uy
+        y_top = HertzContactBC._get_y_top(t)
+
+        res = (False, 0.0)
+
+        if y_deformed >= y_top:
+            self._max_x_with_y_excess = max(self._max_x_with_y_excess, x)
+
+            if x <= self._a_est:
+                res = (True, y_top - y)
+
+        return res
+
+
+    @staticmethod
+    def _get_y_top(t):
+        """Returns the y-position of the contact area depending on the load step t."""
+
+        return 1.0 - 0.005 * t
+
+
+# instantiate the BC object used by OpenGeoSys
+bc = HertzContactBC()
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/post.py b/Tests/Data/Mechanics/Linear/PythonHertzContact/post.py
new file mode 100755
index 0000000000000000000000000000000000000000..8dc81bf1acb607fce9a338edc6f4bb267aff5f07
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/post.py
@@ -0,0 +1,393 @@
+#!/usr/bin/vtkpython
+
+from __future__ import print_function
+import vtk
+from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk
+import numpy as np
+from scipy.interpolate import interp1d
+
+import matplotlib.pyplot as plt
+
+pvd_file = "out/hertz_pcs_0.pvd"
+
+
+R12 = 1.0
+R = R12 / 2.0
+nu_12 = 0.3
+E_12 = 1.0
+
+lambda_ = E_12 * nu_12 / (1 + nu_12) / (1 - 2 * nu_12)
+mu = E_12 / 2.0 / (1 + nu_12)
+G_12 = mu
+
+kappa = 0.5 * G_12 / R / (1-nu_12)
+print("kappa:", kappa)
+
+C = lambda_ * np.matrix([
+    1, 1, 1, 0,
+    1, 1, 1, 0,
+    1, 1, 1, 0,
+    0, 0, 0, 0 ]).reshape(4, 4) \
+            + 2 * mu * np.identity(4)
+
+def p_contact(r, r_contact):
+    return kappa * np.sqrt(r_contact**2 - r**2)
+
+
+### helpers ##############################################
+
+import os
+try:
+    import xml.etree.cElementTree as ET
+except:
+    import xml.etree.ElementTree as ET
+
+
+def relpathfrom(origin, relpath):
+    if os.path.isabs(relpath):
+        return relpath
+    return os.path.join(origin, relpath)
+
+def read_pvd_file(fn):
+    try:
+        path = fn.name
+    except AttributeError:
+        path = fn
+    pathroot = os.path.dirname(path)
+    pvdtree = ET.parse(fn)
+    node = pvdtree.getroot()
+    if node.tag != "VTKFile": return None, None
+    children = list(node)
+    if len(children) != 1: return None, None
+    node = children[0]
+    if node.tag != "Collection": return None, None
+
+    ts = []
+    fs = []
+
+    for child in node:
+        if child.tag != "DataSet": return None, None
+        ts.append(float(child.get("timestep")))
+        fs.append(relpathfrom(pathroot, child.get("file")))
+
+    return ts, fs
+
+### helpers end ##########################################
+
+
+def get_y_top(t):
+    return 1.0 - 0.005 * t
+
+ts, fns = read_pvd_file(pvd_file)
+
+reader = vtk.vtkXMLUnstructuredGridReader()
+
+destroyTopology = vtk.vtkShrinkFilter()
+destroyTopology.SetShrinkFactor(1.0)
+# destroyTopology.SetInputConnection(strainFilter.GetOutputPort())
+# destroyTopology.SetInputConnection(reader.GetOutputPort())
+
+# strainFilter = vtk.vtkCellDerivatives()
+# strainFilter.SetVectorModeToPassVectors()
+# strainFilter.SetTensorModeToComputeStrain()
+# strainFilter.SetOutputPointsPrecision(vtk.vtkAlgorithm.DOUBLE_PRECISION)
+
+warpVector = vtk.vtkWarpVector()
+# warpVector.SetInputConnection(strainFilter.GetOutputPort())
+
+# cell2point = vtk.vtkCellDataToPointData()
+# cell2point.SetInputConnection(destroyTopology.GetOutputPort())
+# cell2point.PassCellDataOff()
+# cell2point.SetOutputPointsPrecision(vtk.vtkAlgorithm.DOUBLE_PRECISION)
+
+plane = vtk.vtkPlane()
+# plane.SetOrigin(0, 0, 0)
+plane.SetNormal(0, 1, 0)
+
+cutter = vtk.vtkCutter()
+cutter.SetCutFunction(plane)
+cutter.SetInputConnection(warpVector.GetOutputPort())
+cutter.SetOutputPointsPrecision(vtk.vtkAlgorithm.DOUBLE_PRECISION)
+
+writer = vtk.vtkXMLUnstructuredGridWriter()
+
+ws = []
+rs_contact = []
+Fs = []
+
+fig, ax = plt.subplots()
+
+fig.subplots_adjust(right=0.75)
+ax.set_xlabel(r"$\xi$ / m")
+ax.set_ylabel(r"$\bar p$ / Pa")
+add_leg = True
+
+
+for t, fn in zip(ts, fns):
+    print("###### time", t)
+    reader.SetFileName(fn)
+    reader.Update()
+    grid = reader.GetOutput()
+
+    disp_2d = vtk_to_numpy(grid.GetPointData().GetArray("displacement"))
+    disp_3d = np.zeros((disp_2d.shape[0], 3))
+    disp_3d[:,(0,1)] = disp_2d
+    disp_3d_vtk = numpy_to_vtk(disp_3d, 1)
+    disp_3d_vtk.SetName("u")
+
+    grid.GetPointData().AddArray(disp_3d_vtk)
+    # grid.GetPointData().SetActiveVectors("u")
+
+    if False:
+        # compute strain
+        def strain_triangle_axi(cell, point_data, strain_data):
+            cell_pts = np.matrix(vtk_to_numpy(cell.GetPoints().GetData())[:,:-1])
+            assert cell_pts.shape[0] == 3  # triangles
+            assert point_data.shape[1] == 2  # 2D vector field
+            node_ids = [ cell.GetPointId(i) for i in range(cell.GetNumberOfPoints()) ]
+
+            # interpolation using barycentric coordinates on linear triangles
+            T = np.matrix(np.empty((2,2)))
+            T[:,0] = (cell_pts[0,:] - cell_pts[2,:]).T
+            T[:,1] = (cell_pts[1,:] - cell_pts[2,:]).T
+            T_inv = np.linalg.inv(T)
+
+            dl1 = T_inv[0,:]  # row 0
+            dl2 = T_inv[1,:]  # row 1
+
+            for node in range(3):
+                l1, l2 = T_inv * (cell_pts[node, :].T - cell_pts[2, :].T)
+                assert -1e-15 < l1 and 1 + 1e-15 > l1 \
+                        and -1e-15 < l2 and 1+ 1e-15 > l2
+
+            grad = np.empty((2,2))
+            for comp in range(2):
+                nodal_values = point_data[node_ids, comp]
+                # nodal_values = cell_pts[:, comp].flat
+                # if t > 0 and cell_pts[0,1] > 0.95 and comp == 1:
+                #     print(nodal_values[0])
+                grad[comp,:] = dl1 * nodal_values[0] \
+                        + dl2 * nodal_values[1] \
+                        - (dl1 + dl2) * nodal_values[2]
+
+            # if t > 0 and cell_pts[0,1] > 0.95:
+            #     print(grad)
+
+            strain = 0.5 * (grad + grad.T)  # rr, rz, zr,zz components
+
+            for node in range(3):
+                r = cell_pts[node, 0]
+                node_id = node_ids[node]
+
+                if r == 0:
+                    dvdr = grad[0,0]
+                    v_over_r = dvdr
+                else:
+                    v_over_r = point_data[node_id, 0] / r
+
+                strain_kelvin = np.array([
+                    strain[0,0], strain[1,1], v_over_r,
+                    strain[0,1] * np.sqrt(2.0)
+                    ])
+                strain_data[node_id, :] = strain_kelvin
+
+        def computeStrain(grid):
+            destroyTopology.SetInputData(grid)
+            destroyTopology.Update()
+            grid = destroyTopology.GetOutput()
+
+            disp_2d = vtk_to_numpy(grid.GetPointData().GetArray("displacement"))
+            strain_kelvin = np.empty((disp_2d.shape[0], 4))
+
+            n_cells = grid.GetNumberOfCells()
+            for c in xrange(n_cells):
+                cell = grid.GetCell(c)
+                strain_triangle_axi(cell, disp_2d, strain_kelvin)
+
+            strain_kelvin_vtk = numpy_to_vtk(strain_kelvin, 1)
+            strain_kelvin_vtk.SetName("strain_post_kelvin")
+            grid.GetPointData().AddArray(strain_kelvin_vtk)
+
+            strain = strain_kelvin.copy()
+            strain[:,3] /= np.sqrt(2.0)
+            strain_vtk = numpy_to_vtk(strain, 1)
+            strain_vtk.SetName("strain_post")
+            grid.GetPointData().AddArray(strain_vtk)
+
+            # ( (4 x 4) * (nodes x 4).T ).T
+            stress_kelvin = (C * strain_kelvin.T).T
+            # stress_kelv = np.empty_like(strain_kelv)
+            # for c, eps in enumerate(strain_kelv):
+            #     stress_kelv[c, :] = (C * np.atleast_2d(eps).T).flat
+
+            stress_kelvin_vtk = numpy_to_vtk(stress_kelvin, 1)
+            stress_kelvin_vtk.SetName("stress_post_kelvin")
+
+            stress_symm_tensor = stress_kelvin.copy()
+            stress_symm_tensor[:,3] /= np.sqrt(2.0)
+
+            stress_symm_tensor_vtk = numpy_to_vtk(stress_symm_tensor, 1)
+            stress_symm_tensor_vtk.SetName("stress_post")
+            grid.GetPointData().AddArray(stress_symm_tensor_vtk)
+
+            writer.SetInputData(grid)
+            writer.SetFileName(os.path.join(
+                os.path.dirname(fn), "post_{:.0f}.vtu".format(t)))
+            writer.Write()
+
+            return grid
+
+        grid = computeStrain(grid)
+
+    grid.GetPointData().SetActiveVectors("u")
+    warpVector.SetInputData(grid)
+    warpVector.Update()
+    grid = warpVector.GetOutput()
+
+    xmin, xmax, ymin, ymax, zmin, zmax = grid.GetBounds()
+    y_top = get_y_top(t)
+    try:
+        assert abs(ymax - y_top) < 1e-7
+    except:
+        print(ymax, y_top, ymax - y_top)
+        raise
+    ws.append(2 * (1.0 - y_top))
+
+    # determine top boundary
+    coords = vtk_to_numpy(grid.GetPoints().GetData())
+    assert abs(min(coords[:,0])) < 1e-8
+    idcs_top_boundary = np.where(coords[:,1] > y_top - 1e-7)[0]
+    # print(idcs_top_boundary)
+    assert len(idcs_top_boundary) != 0
+    xs_top_boundary = coords[idcs_top_boundary, 0]
+    idx_max = np.argmax(xs_top_boundary)
+    r_contact = max(xs_top_boundary[idx_max], 0.0)
+    y_at_r_contact = coords[idcs_top_boundary[idx_max], 1]
+    print("radius of contact area:", r_contact, "at y =", y_at_r_contact)
+
+    rs_contact.append(r_contact)
+
+    def average_stress(rs, stress):
+        r_contact = max(rs)
+        rs_int = np.linspace(min(rs), max(rs)-1e-8, max(len(rs), 200))
+        stress_int = interp1d(rs, stress, bounds_error=False, fill_value=0.0)
+        avg_stress = np.empty_like(rs_int)
+
+        for i, r in enumerate(rs_int):
+            rho_max = np.sqrt(r_contact**2 - r**2)
+            rhos = np.linspace(0, rho_max, 100)
+            xis = np.sqrt(rhos**2 + r**2)
+            try:
+                assert max(xis) <= r_contact + 1e-8
+            except:
+                print(max(xis), r_contact)
+                raise
+            avg_stress[i] = 1.0 / rho_max * np.trapz(x=rhos, y=stress_int(xis))
+        # avg_stress[-1] = 0.0
+
+        return rs_int, avg_stress
+
+    def total_force(rs, stress):
+        assert all(rs > -1e-6)
+        rs_int = np.linspace(min(rs), max(rs), max(len(rs), 200))
+        stress_int = interp1d(rs, stress, bounds_error=False, fill_value=0.0)
+
+        F = 2.0 * np.pi * np.trapz(x=rs_int, y=rs_int * stress_int(rs_int))
+
+        return F
+
+
+    def stress_at_contact_area():
+        global add_leg
+
+        plane.SetOrigin(0, y_at_r_contact, 0)
+        cutter.Update()
+        grid = cutter.GetOutput()
+
+        # for a in range(grid.GetPointData().GetNumberOfArrays()):
+        #     print(grid.GetPointData().GetArrayName(a))
+
+        xs = vtk_to_numpy(grid.GetPoints().GetData())[:, 0]
+        try:
+            assert abs(max(xs) - r_contact) < 1e-7
+            assert abs(min(xs)) < 1e-8
+        except:
+            print(min(xs), max(xs), r_contact, max(xs) - r_contact)
+            raise
+
+        w_0 = 2.0 * (1.0 - y_top)
+
+        rs = np.linspace(0, r_contact, 200)
+        if add_leg: ax.plot([], [], color="k", ls=":", label="ref")
+        h, = ax.plot(rs, -p_contact(rs, r_contact), ls=":")
+
+        if False:
+            r_contact_ana = np.sqrt(w_0 * R)
+
+            rs2 = np.linspace(0, r_contact_ana, 200)
+            if add_leg: ax.plot([], [], color="k", ls="--", label="ref2")
+            ax.plot(rs2, -p_contact(rs, r_contact_ana), ls="--", color=h.get_color())
+
+        stress_yy = vtk_to_numpy(grid.GetPointData().GetArray("sigma"))[:,1]
+        rs, avg_stress_yy = average_stress(xs, stress_yy)
+        if add_leg: ax.plot([], [], color="k", ls="-", label="ogs")
+        ax.plot(rs, avg_stress_yy, color=h.get_color(), ls="-",
+                label=r"$w_0 = {}$".format(w_0))
+
+        if False:
+            stress = vtk_to_numpy(grid.GetPointData().GetArray("stress_post"))
+            rs, avg_stress_yy = average_stress(xs, stress[:,1])
+            if add_leg: ax.plot([], [], color="k", label="post")
+            ax.plot(rs, avg_stress_yy, color=h.get_color(),
+                    label=r"$w_0 = {}$".format(2*(1.0 - y_top)))
+
+        ax.scatter([r_contact], [0], color=h.get_color())
+        ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))
+
+        fig.savefig("stress_at_contact.png")
+        add_leg = False
+
+        Fs.append(-total_force(xs, stress_yy))
+
+    if t > 0:
+        stress_at_contact_area()
+
+fig.savefig("stress_at_contact.png")
+plt.close(fig)
+
+
+fig, ax = plt.subplots()
+ax.scatter(ws, rs_contact, label="ogs")
+
+ws_ref = np.linspace(0, max(ws), 200)
+rs_ref = np.sqrt(ws_ref * R)
+
+ax.plot(ws_ref, rs_ref, label="ref")
+
+ax.legend()
+
+ax.set_xlabel("displacement of sphere centers $w_0$ / m")
+ax.set_ylabel("radius of contact area $a$ / m")
+
+fig.subplots_adjust(left=0.15, right=0.98)
+fig.savefig("contact_radii.png")
+plt.close(fig)
+
+
+fig, ax = plt.subplots()
+ax.scatter(rs_contact[1:], Fs, label="ogs")
+
+rs_ref = np.linspace(0, max(rs_contact), 200)
+Fs_ref = 8. * rs_ref**3 * kappa / 3.0
+
+ax.plot(rs_ref, Fs_ref, label="ref")
+
+l = ax.legend()
+l.get_frame().set_facecolor('white')
+
+ax.set_xlabel("radius of contact area $a$ / m")
+ax.set_ylabel("applied force $F$ / N")
+
+fig.subplots_adjust(left=0.15, right=0.98)
+fig.savefig("total_force.png")
+plt.close(fig)
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_10.vtu b/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_10.vtu
new file mode 100644
index 0000000000000000000000000000000000000000..7c76d78d096efd3e098be1fc73a906fc313da710
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_10.vtu
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71bc3a358b09c749174bb760d050cc76ab5b6ca748de2b67c54421980b041e00
+size 3679599
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_5.vtu b/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_5.vtu
new file mode 100644
index 0000000000000000000000000000000000000000..fc948ec69f524baebd69398aa396a2875caae011
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/ref_hertz_contact_ts_5.vtu
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0837cb953245e61369946031c78d6e8c1abfe183ed7fc1bb62655be91a173ed
+size 3688217
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.gml b/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.gml
new file mode 100644
index 0000000000000000000000000000000000000000..390741dc42747d99437b5199aae8549464030286
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.gml
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c557c6b2f2dea1cc8d7be0a258a344ea5f14172996789fdfe3f43bfee738938
+size 20651
diff --git a/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.vtu b/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.vtu
new file mode 100644
index 0000000000000000000000000000000000000000..7d98625cf830548a032f6f54a98aba6ed117243e
--- /dev/null
+++ b/Tests/Data/Mechanics/Linear/PythonHertzContact/unit-circle.vtu
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa46da4364e860a71edfbd753adce21ca60a5c75a8890df25d847e28e1d7036a
+size 326511
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/contact_radii.png b/web/content/docs/benchmarks/python-bc/hertz-contact/contact_radii.png
new file mode 100644
index 0000000000000000000000000000000000000000..92b7664b73b9b299284ba979ef6f0e69384dc7eb
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/contact_radii.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f45567cca7e784dba58159935b5370cd3b4c28d882fb486f6110f926f0e2ef2
+size 21649
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.md b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.md
new file mode 100644
index 0000000000000000000000000000000000000000..c249033c4300d8dc7cbc86cf44b540de77f519c9
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.md
@@ -0,0 +1,100 @@
++++
+project = "https://github.com/ufz/ogs-data/blob/master/Mechanics/Linear/Python/hertz-contact.prj"
+author = "Christoph Lehmann"
+date = "2018-08-06T11:41:00+02:00"
+title = "Hertz Contact using Python Boundary Conditions"
+weight = 4
+
+[menu]
+  [menu.benchmarks]
+    parent = "python-bc"
+
++++
+
+{{< data-link >}}
+
+## Motivation of this test case
+
+The aim of this test is:
+
+* to show that it is possible to apply Dirichlet BCs at a boundary that changes over the course of the simulation
+* to give an advanced use-case of the Python BC.
+  Here essentially an iterative procedure for a contact problem is implemented
+  within the Python BC.
+
+## Problem description
+
+Two elastic spheres of same radius $R$ are brought into contact.
+The sphere centers are displaced towards each other by $w\_0$, with increasing
+values in every load step.
+Due to symmetry reasons a flat circular contact area of radius $a$ forms.
+
+{{< img src="../hertz-contact.png" >}}
+
+The contact between the two spheres is modelled as a Dirichlet BC
+on a varying boundary. The exact boundary and Dirichlet values for the
+$y$ displacements are determined in a Python script.
+Compared to the sketch above the prescribed $y$ displacements correspond
+to $w\_0/2$, because due to symmetry only half of the system (a section of the
+lower sphere) is simulated.
+
+
+## Analytical solution
+
+The derivation of the formulae below can be found, e.g.,
+in [this book (in German)](http://www.uni-magdeburg.de/ifme/l-festigkeit/pdf/Bertram-Gluege_Festkoerpermechanik2012.pdf).
+
+The radius of the contact area is given by
+$$
+\begin{equation}
+a = \sqrt{\frac{w\_0 R}{2}}
+\end{equation}
+$$
+
+The average pressure $\bar p$ over a the secant with distance $\xi$ to the
+center of the contact area (cf. vertical dashed line in the sketch above) is assumed to be
+$$
+\begin{equation}
+\bar p(\xi) = \kappa \sqrt{a^2 - \xi^2}
+\label{eq:bar-p}
+\end{equation}
+$$
+with the prefactor $\kappa$ given by
+$$
+\begin{equation}
+\kappa = \frac{G}{R \cdot (1-\nu)}
+\,.
+\end{equation}
+$$
+
+The total force $F$ exerted on the contact area is given by
+$$
+\begin{equation}
+F = \frac{8 a^3}{3\kappa}
+\,.
+\end{equation}
+$$
+
+
+## Results
+
+Contact radii:
+
+{{<img src="../contact_radii.png">}}
+
+Average pressure $\bar{p}$:
+
+{{<img src="../stress_at_contact.png">}}
+
+Total force $F$:
+
+{{<img src="../total_force.png">}}
+
+The simulation results for contact radii and total force reproduce the
+analytical square root and cubic laws, respectively.
+For the average pressure $\bar p$ the analytical form of
+Eq.&nbsp;$(\ref{eq:bar-p})$ is reproduced, too.
+But for $\bar p$ there are rather strong deviations between the numerical
+and analytical results, which might be due to deviations in the
+contact radii&nbsp;$a$, due to unsufficient mesh resolution or due to
+the chosen linear order of FEM ansatz functions.
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.png b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.png
new file mode 100644
index 0000000000000000000000000000000000000000..336d096192a3f3a931729057fb2e2e15eebe3df4
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d129535c6bbe64e46936b8760f71720d293ee206ce617bff6a06160c1b25b5ef
+size 21644
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.svg b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.svg
new file mode 100644
index 0000000000000000000000000000000000000000..07dc91e4480d299fa11260be2401b008b71d9a92
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.svg
@@ -0,0 +1,382 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="241.4953"
+   height="199.97981"
+   viewBox="0 0 63.895632 52.911329"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.2 2405546, 2018-03-11"
+   sodipodi:docname="hertz-contact.svg"
+   inkscape:export-filename="/home/lehmannc/prog/ogs6/github-chleh-PRs/src/web/content/docs/benchmarks/python-bc/hertz-contact/hertz-contact.png"
+   inkscape:export-xdpi="200"
+   inkscape:export-ydpi="200">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="marker5094"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5092"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)" />
+    </marker>
+    <marker
+       inkscape:isstock="true"
+       style="overflow:visible"
+       id="marker4996"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Mend"
+       inkscape:collect="always">
+      <path
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path4994"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="marker4544"
+       style="overflow:visible"
+       inkscape:isstock="true"
+       inkscape:collect="always">
+      <path
+         id="path4542"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:isstock="true"
+       style="overflow:visible"
+       id="marker4136"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Mend">
+      <path
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path4134"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="marker3552"
+       style="overflow:visible"
+       inkscape:isstock="true"
+       inkscape:collect="always">
+      <path
+         id="path3550"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:isstock="true"
+       style="overflow:visible"
+       id="marker3052"
+       refX="0"
+       refY="0"
+       orient="auto"
+       inkscape:stockid="Arrow1Mend"
+       inkscape:collect="always">
+      <path
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         id="path3050"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true"
+       inkscape:collect="always">
+      <path
+         id="path833"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mstart"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mstart"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path830"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(0.4,0,0,0.4,4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Send"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Send"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path839"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Sstart"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Sstart"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path836"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(0.2,0,0,0.2,1.2,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="159.92526"
+     inkscape:cy="104.36074"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1600"
+     inkscape:window-height="875"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     units="px"
+     inkscape:snap-global="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-38.044515,-206.59195)">
+    <path
+       d="m 53.67722,228.37355 a 15.497022,15.497022 0 0 1 15.497021,15.49702 H 53.677219 Z"
+       sodipodi:end="0"
+       sodipodi:start="4.712389"
+       sodipodi:ry="15.497022"
+       sodipodi:rx="15.497022"
+       sodipodi:cy="243.87057"
+       sodipodi:cx="53.677219"
+       sodipodi:type="arc"
+       id="path4075"
+       style="opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <circle
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="path815"
+       cx="-85.251534"
+       cy="212.12242"
+       r="15.497022"
+       transform="rotate(-35.474654)" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.52916665, 0.52916665;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 42.472232,233.04379 H 64.774705"
+       id="path818"
+       inkscape:connector-curvature="0" />
+    <circle
+       r="15.497022"
+       cy="50.393448"
+       cx="244.57024"
+       id="circle820"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       transform="rotate(65.944031)" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow1Mend);marker-end:url(#Arrow1Mstart)"
+       d="m 53.677221,228.20874 v 9.61452"
+       id="path1360"
+       inkscape:connector-curvature="0" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="54.004654"
+       y="230.85077"
+       id="text3030"><tspan
+         sodipodi:role="line"
+         id="tspan3028"
+         x="54.004654"
+         y="230.85077"
+         style="stroke-width:0.26458332px"><tspan
+           style="font-style:oblique;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans, Oblique';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start"
+           id="tspan3032">w</tspan><tspan
+           style="font-size:64.99999762%;baseline-shift:sub"
+           id="tspan3034">0</tspan></tspan></text>
+    <circle
+       r="11.018708"
+       cy="238.72971"
+       cx="-37.353424"
+       id="circle3040"
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       transform="rotate(-30.961967)" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker3052)"
+       d="m 90.887678,223.87032 9.015421,-5.40887"
+       id="path3042"
+       inkscape:connector-curvature="0" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="94.475052"
+       y="219.86319"
+       id="text3098"><tspan
+         sodipodi:role="line"
+         id="tspan3096"
+         x="94.475052"
+         y="219.86319"
+         style="font-style:oblique;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans, Oblique';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332px">a</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="85.289948"
+       y="227.97792"
+       id="text3540"><tspan
+         sodipodi:role="line"
+         id="tspan3538"
+         x="85.289948"
+         y="227.97792"
+         style="stroke-width:0.26458332px">contact area</tspan></text>
+    <path
+       inkscape:connector-curvature="0"
+       id="path3548"
+       d="m 60.050003,232.8548 19.1589,-5.6054"
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker3552)"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker4136)"
+       d="m 60.317273,241.40742 16.35257,2.67996"
+       id="path4132"
+       inkscape:connector-curvature="0" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="78.176338"
+       y="244.88242"
+       id="text4930"><tspan
+         sodipodi:role="line"
+         id="tspan4928"
+         x="78.176338"
+         y="244.88242"
+         style="stroke-width:0.26458332px">simulation domain</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker4544)"
+       d="m 53.461273,222.37563 12.519405,-8.92165"
+       id="path4932"
+       inkscape:connector-curvature="0" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path4992"
+       d="m 53.569132,243.62843 6.266508,14.03789"
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker4996)" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="55.7257"
+       y="218.15546"
+       id="text5078"><tspan
+         sodipodi:role="line"
+         id="tspan5076"
+         x="55.7257"
+         y="218.15546"
+         style="font-style:oblique;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans, Oblique';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332px">R</tspan></text>
+    <text
+       id="text5082"
+       y="252.09869"
+       x="53.854813"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       xml:space="preserve"><tspan
+         style="font-style:oblique;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans, Oblique';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332px"
+         y="252.09869"
+         x="53.854813"
+         id="tspan5080"
+         sodipodi:role="line">R</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker5094)"
+       d="M 90.865278,223.93005 H 83.581564"
+       id="path5084"
+       inkscape:connector-curvature="0" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:6.61458302px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       x="87.559395"
+       y="222.74509"
+       id="text5200"><tspan
+         sodipodi:role="line"
+         id="tspan5198"
+         x="87.559395"
+         y="222.74509"
+         style="font-style:oblique;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:'Latin Modern Sans';-inkscape-font-specification:'Latin Modern Sans, Oblique';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332px">ξ</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.52916661, 0.52916661;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 83.023678,216.03601 v 15.63875"
+       id="path5316"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/stress_at_contact.png b/web/content/docs/benchmarks/python-bc/hertz-contact/stress_at_contact.png
new file mode 100644
index 0000000000000000000000000000000000000000..3aa8f0146621819024074f347e147de509822b28
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/stress_at_contact.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ec5b3f1339f960f105b99b7bd092ce00bc985bb52624df51df9182b7d4b6e65
+size 86699
diff --git a/web/content/docs/benchmarks/python-bc/hertz-contact/total_force.png b/web/content/docs/benchmarks/python-bc/hertz-contact/total_force.png
new file mode 100644
index 0000000000000000000000000000000000000000..2cb423ffa402b68fb81373ba505361c9ccf0c662
--- /dev/null
+++ b/web/content/docs/benchmarks/python-bc/hertz-contact/total_force.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4284b5904a527b7e5228fb45b7d211064f9a373764f4096ed0e2e7a13fb225a3
+size 19421