linear_elasticity/two_bodies_contact.py

Description

Contact of two elastic bodies with a penalty function for enforcing the contact constraints.

Find \ul{u} such that:

\int_{\Omega} D_{ijkl}\ e_{ij}(\ul{v}) e_{kl}(\ul{u})
+ \int_{\Gamma_{c}} \varepsilon_N \langle g_N(\ul{u}) \rangle \ul{n} \ul{v}
= 0
\;, \quad \forall \ul{v} \;,

where \varepsilon_N \langle g_N(\ul{u}) \rangle is the penalty function, \varepsilon_N is the normal penalty parameter, \langle
g_N(\ul{u}) \rangle are the Macaulay’s brackets of the gap function g_N(\ul{u}) and

D_{ijkl} = \mu (\delta_{ik} \delta_{jl}+\delta_{il} \delta_{jk}) +
\lambda \ \delta_{ij} \delta_{kl}
\;.

This example also demonstrates use of an experimental contact term based on the IPC Toolkit [1].

[1] https://github.com/ipc-sim/ipc-toolkit

Usage examples:

# Check regions and EBC nodes.
sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py --save-regions-as-groups --save-ebc-nodes --solve-not

sfepy-view two_bodies_ebc_nodes.vtk
sfepy-view two_bodies_regions.h5

# Run with default parameters and view results.
sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py

sfepy-view output/contact/two_bodies.h5
sfepy-view output/contact/two_bodies.h5 -f u:wu:f1:p0 1:vw:wu:f1:p0

# Plot the nonlinear solver convergence.
python3 sfepy/scripts/plot_logs.py output/contact/log.txt

# Run with default parameters and IPC toolkit, view results.
sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py -d "contact='ipc'"
sfepy-view output/contact/two_bodies.h5 -f u:wu:f1:p0 1:vw:wu:f1:p0

# Plot the IPC contact parameters evolution.
python3 sfepy/scripts/plot_logs.py output/contact/clog.txt
../_images/linear_elasticity-two_bodies_contact1.png

source code

r"""
Contact of two elastic bodies with a penalty function for enforcing the contact
constraints.

Find :math:`\ul{u}` such that:

.. math::
    \int_{\Omega} D_{ijkl}\ e_{ij}(\ul{v}) e_{kl}(\ul{u})
    + \int_{\Gamma_{c}} \varepsilon_N \langle g_N(\ul{u}) \rangle \ul{n} \ul{v}
    = 0
    \;, \quad \forall \ul{v} \;,

where :math:`\varepsilon_N \langle g_N(\ul{u}) \rangle` is the penalty
function, :math:`\varepsilon_N` is the normal penalty parameter, :math:`\langle
g_N(\ul{u}) \rangle` are the Macaulay's brackets of the gap function
:math:`g_N(\ul{u})` and

.. math::
    D_{ijkl} = \mu (\delta_{ik} \delta_{jl}+\delta_{il} \delta_{jk}) +
    \lambda \ \delta_{ij} \delta_{kl}
    \;.

This example also demonstrates use of an experimental contact term based on the
IPC Toolkit [1].

[1] https://github.com/ipc-sim/ipc-toolkit

Usage examples::

  # Check regions and EBC nodes.
  sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py --save-regions-as-groups --save-ebc-nodes --solve-not

  sfepy-view two_bodies_ebc_nodes.vtk
  sfepy-view two_bodies_regions.h5

  # Run with default parameters and view results.
  sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py

  sfepy-view output/contact/two_bodies.h5
  sfepy-view output/contact/two_bodies.h5 -f u:wu:f1:p0 1:vw:wu:f1:p0

  # Plot the nonlinear solver convergence.
  python3 sfepy/scripts/plot_logs.py output/contact/log.txt

  # Run with default parameters and IPC toolkit, view results.
  sfepy-run sfepy/examples/linear_elasticity/two_bodies_contact.py -d "contact='ipc'"
  sfepy-view output/contact/two_bodies.h5 -f u:wu:f1:p0 1:vw:wu:f1:p0

  # Plot the IPC contact parameters evolution.
  python3 sfepy/scripts/plot_logs.py output/contact/clog.txt
"""
import os.path as op
from functools import partial
import inspect

from sfepy.base.base import output
from sfepy.base.log import Log
from sfepy.mechanics.matcoefs import stiffness_from_youngpoisson
from sfepy.discrete.fem.meshio import UserMeshIO

import numpy as nm

def get_bbox(dims, centre, eps=0.0):
    dims = nm.asarray(dims)
    centre = nm.asarray(centre)

    bbox = nm.r_[[centre - (0.5 - eps) * dims], [centre + (0.5 - eps) * dims]]
    return bbox

def gen_two_bodies(dims0, shape0, centre0, dims1, shape1, centre1, shift1):
    from sfepy.discrete.fem import Mesh
    from sfepy.mesh.mesh_generators import gen_block_mesh

    m0 = gen_block_mesh(dims0, shape0, centre0)
    m1 = gen_block_mesh(dims1, shape1, centre1)

    coors = nm.concatenate((m0.coors, m1.coors + shift1), axis=0)

    desc = m0.descs[0]
    c0 = m0.get_conn(desc)
    c1 = m1.get_conn(desc)
    conn = nm.concatenate((c0, c1 + m0.n_nod), axis=0)

    ngroups = nm.zeros(coors.shape[0], dtype=nm.int32)
    ngroups[m0.n_nod:] = 1

    mat_id = nm.zeros(conn.shape[0], dtype=nm.int32)
    mat_id[m0.n_el:] = 1

    name = 'two_bodies'

    mesh = Mesh.from_data(name, coors, ngroups, [conn], [mat_id], m0.descs)

    return mesh

def find_contact_ipc_term(equations):
    for eq in equations:
        for term in eq.terms:
            if term.name == 'dw_contact_ipc':
                break

        else:
            continue

        break

    else:
        raise ValueError('no dw_contact_ipc in equations!')

    return term

def apply_line_search(vec_x0, vec_dx0, it, err_last, conf, fun,
                      timers, log=None, context=None, clog=None):
    """
    Apply a backtracking line-search with continuous collision detection from
    IPC toolkit.
    """
    pb = context
    term = find_contact_ipc_term(pb.equations)

    select_other = lambda term: term.name != 'dw_contact_ipc'

    ls = 1.0
    vec_dx = vec_dx0

    variables = pb.get_variables()

    ci = term.get_contact_info(variables['u'])
    ci.it = it

    ls_it = 0
    while 1:
        vec_x = vec_x0 - vec_dx
        if it > 0:
            # Determine max. step size using continuous collision detection.
            u0 = variables.make_full_vec(vec_x0).reshape((-1, ci.dim))
            u1 = variables.make_full_vec(vec_x).reshape((-1, ci.dim))
            x0 = ci.smesh.coors + u0[ci.nods]
            x1 = ci.smesh.coors + u1[ci.nods]

            max_step_size = term.ipc.compute_collision_free_stepsize(
                ci.collision_mesh, x0, x1,
            )
            if max_step_size < 1.0:
                vec_x = vec_x0 - max_step_size * vec_dx

            ls = min(ls, max_step_size)

        timers.residual.start()

        ci.ls_it = ls_it

        try:
            vec_r1 = fun(vec_x, select_term=select_other)

            vec_r1_full = pb.equations.make_full_vec(vec_r1, force_value=0.0)
            ci.e_grad_full = vec_r1_full

            val, iels = term.evaluate(mode='weak', dw_mode='vector',
                                      standalone=False, ci=ci)

            vec_r2 = nm.zeros_like(vec_r1)
            term.assemble_to(vec_r2, val, iels, mode='vector')

            vec_r = vec_r1 + vec_r2

        except ValueError:
            if (it == 0) or (ls < conf.ls_min):
                output('giving up!')
                raise

            else:
                ok = False

        else:
            ok = True

        timers.residual.stop()

        if ok:
            err = nm.linalg.norm(vec_r)
            if not nm.isfinite(err):
                output('residual:', vec_r)
                output(nm.isfinite(vec_r).all())
                raise ValueError('infs or nans in the residual')

            if log is not None:
                log(err, it)

            if clog is not None:
                if it > 0:
                    clog(ci.min_distance, ci.barrier_stiffness,
                         ci.bp_grad_norm, ci.e_grad_norm)

                else:
                    clog(nm.nan, nm.nan, nm.nan, nm.nan)

            if (it == 0) or (err < (err_last * conf.ls_on)):
                break

            red = conf.ls_red
            output('linesearch: iter %d, (%.5e < %.5e) (new ls: %e)'
                   % (it, err, err_last * conf.ls_on, red * ls))

        else: # Failure.
            if conf.give_up_warp:
                output('giving up!')
                break

            red = conf.ls_red_warp
            output('residual computation failed for iter %d'
                   ' (new ls: %e)!' % (it, red * ls))

        if ls < conf.ls_min:
            output('linesearch failed, continuing anyway')
            break

        ls *= red
        vec_dx = ls * vec_dx0
        ls_it += 1

    return vec_x, vec_r, err, ok

def markdown_table_from_dict(adict):
    header = '| option | value |\n'
    separator = '| --- | --- |\n'
    rows = '\n'.join([f'| {key} | {val} |' for key, val in adict.items()])

    return header + separator + rows

def define(
        dims0=(1.0, 1.0, 0.5),
        shape0=(2, 2, 2),
        centre0=(0, 0, -0.25),

        dims1=(1.2, 0.8, 0.5),
        shape1=(3, 3, 2),
        centre1=(0, 0, 0.25),

        shift10=(0.0, 0.0, 1e-4),
        shift11=(0.0, 0.0, -0.1),

        young=1.0,
        poisson=0.3,
        rho=1.0,

        cm=None,
        ck=0.0,
        dhat=1e-2,
        pspd='NONE',

        t1=1000,
        n_step=5,
        contact='builtin',

        output_dir='output/contact',
        verbose=True,
):
    args = locals()

    inodir = partial(op.join, output_dir)

    output.set_output(filename=inodir('output_log.txt'), quiet=not verbose,
                      combined=True)

    signature = inspect.signature(define)
    arg_names = list(signature.parameters.keys())
    options = {name : args[name] for name in arg_names}
    table = markdown_table_from_dict(options)
    with open(inodir('options.md'), 'w') as fd:
        fd.write(table)

    dim = len(dims0)
    shape0 = shape0[:dim]
    centre0 = centre0[:dim]

    dims1 = dims1[:dim]
    shape1 = shape1[:dim]
    centre1 = centre1[:dim]

    shift10 = shift10[:dim]
    shift11 = shift11[:dim]

    shift11 = nm.array(shift11)

    clog = Log([[r'$d$'], [r'$k$'], [r'$\nabla B$'], [r'$\nabla E$']],
               xlabels=['', '', 'all iterations', 'all iterations'],
               ylabels=[r'$d$', r'$k$', r'$\nabla B$', r'$\nabla E$'],
               yscales=['log', 'linear', 'linear', 'linear'],
               is_plot=False,
               log_filename=inodir('clog.txt'),
               formats=[['%.8e']] * 4)

    def mesh_hook(mesh, mode):
        if mode == 'read':
            return gen_two_bodies(dims0, shape0, centre0,
                                  dims1, shape1, centre1, shift10)

        elif mode == 'write':
            pass

    def post_process(out, pb, state, extend=False):
        from sfepy.base.base import Struct
        from sfepy.discrete.fem import extend_cell_data

        ev = pb.evaluate
        gap = ev('dw_contact.i.Contact(contact.epss, v, u)',
                 mode='el_avg', term_mode='gap')
        gap = extend_cell_data(gap, pb.domain, 'Contact', val=0.0,
                               is_surface=True)
        out['gap'] = Struct(name='output_data',
                            mode='cell', data=gap, dofs=None)

        return out

    filename_mesh = UserMeshIO(mesh_hook)

    options = {
        'nls' : 'newton',
        'ls' : 'ls',
        'output_dir' : output_dir,
        'output_format' : 'h5',
        'post_process_hook' : 'post_process',
    }

    bbox0 = get_bbox(dims0, centre0, eps=1e-5)
    bbox1 = get_bbox(dims1, nm.asarray(centre1) + nm.asarray(shift10), eps=1e-5)

    if dim == 2:
        regions = {
            'Omega' : 'all',
            'Omega0' : 'cells of group 0',
            'Omega1' : 'cells of group 1',
            'Bottom' : ('vertices in (y < %.12e)' % bbox0[0, 1], 'facet'),
            'Top' : ('vertices in (y > %.12e)' % bbox1[1, 1], 'facet'),
            'Contact0' : ('(vertices in (y > %.12e) *v r.Omega0)' % bbox0[1, 1],
                          'facet'),
            'Contact1' : ('(vertices in (y < %.12e) *v r.Omega1)' % bbox1[0, 1],
                          'facet'),
            'Contact' : ('r.Contact0 +s r.Contact1', 'facet')
        }

    else:
        regions = {
            'Omega' : 'all',
            'Omega0' : 'cells of group 0',
            'Omega1' : 'cells of group 1',
            'Bottom' : ('vertices in (z < %.12e)' % bbox0[0, 2], 'facet'),
            'Top' : ('vertices in (z > %.12e)' % bbox1[1, 2], 'facet'),
            'Contact0' : ('(vertices in (z > %.12e) *v r.Omega0)' % bbox0[1, 2],
                          'facet'),
            'Contact1' : ('(vertices in (z < %.12e) *v r.Omega1)' % bbox1[0, 2],
                          'facet'),
            'Contact' : ('r.Contact0 +s r.Contact1', 'facet')
        }

    fields = {
        'displacement': ('real', dim, 'Omega', 1),
    }

    variables = {
        'u' : ('unknown field', 'displacement', 0),
        'v' : ('test field', 'displacement', 'u'),
    }

    ebcs = {
        'fixb' : ('Bottom', {'u.all' : 0.0}),
        'fixt' : ('Top', {'u.all' : 'move_top'}),
    }

    def move_top(ts, coors, bc, problem, **kwargs):
        val = nm.empty_like(coors)
        val[:] = ts.nt * shift11
        return val

    functions = {
        'move_top' : (move_top,),
    }

    volume = nm.prod(dims0) + nm.prod(dims1)
    mass = volume * rho

    materials = {
        'solid' : ({
            'D' : stiffness_from_youngpoisson(dim, young=young, poisson=poisson),
        },),
        'contact' : ({
            '.m' : cm if cm is not None else mass,
            '.k' : ck, # 0 = Adaptive barrier stiffness.
            '.dhat' : dhat,
            '.Pspd' : pspd,
            '.epss' : 2e+1,
        },),
    }

    integrals = {
        'i' : 2,
    }

    if contact == 'builtin':
        equations = {
            'elasticity' :
            """
               dw_lin_elastic.2.Omega(solid.D, v, u)
             + dw_contact.i.Contact(contact.epss, v, u)
             = 0
            """,
        }

    elif contact == 'ipc':
        equations = {
            'elasticity' :
            """
               dw_lin_elastic.2.Omega(solid.D, v, u)
             + dw_contact_ipc.i.Contact(
                   contact.m, contact.k, contact.dhat, contact.Pspd, v, u
               )
             = 0
            """,
        }

    else:
        raise ValueError("contact argument can be one of 'builtin', 'ipc'!")

    if contact == 'ipc':
        apply_ls = partial(apply_line_search, clog=clog)

    else:
        apply_ls = None

    solvers = {
        'ls' : ('ls.auto_direct', {}),
        'newton' : ('nls.newton', {
            'i_max' : 30,
            'eps_a' : 1e-8,
            'eps_r' : 1e-8,
            'eps_mode' : 'or',
            'macheps' : 1e-16,
            # Linear system error < (eps_a * lin_red).
            'lin_red' : None,
            'line_search_fun' : apply_ls,
            'ls_red' : 0.5,
            'ls_red_warp' : 0.5,
            'ls_on' : 1.0,
            'ls_min' : 1e-5,
            'check' : 0,
            'delta' : 1e-8,
            'log' : {'text' : inodir('log.txt')},
            'log_vlines' : 'solve',
        }),
        'ts' : ('ts.simple', {
            't0'     : 0.0,
            't1'     : t1,
            'dt'     : None,
            'n_step' : n_step,
            'quasistatic' : True,
            'verbose' : 1,
        }),
    }

    return locals()