def test_analytical(monitor, method, plot_mesh=False):
    """
    Check that the moved mesh matches that obtained previously.
    """
    fname = '_'.join([monitor.__name__, method])
    fpath = os.path.dirname(__file__)

    op = Options(approach='monge_ampere', r_adapt_rtol=1.0e-03, nonlinear_method=method)

    mesh = UnitSquareMesh(20, 20)
    orig_vol = assemble(Constant(1.0)*dx(domain=mesh))
    mm = MeshMover(mesh, monitor, op=op)
    mm.adapt()

    mesh.coordinates.assign(mm.x)
    vol = assemble(Constant(1.0)*dx(domain=mesh))
    assert np.allclose(orig_vol, vol), "Volume is not conserved!"

    if plot_mesh:
        import matplotlib.pyplot as plt
        fig, axes = plt.subplots(figsize=(5, 5))
        triplot(mesh, axes=axes, interior_kw={'linewidth': 0.1}, boundary_kw={'color': 'k'})
        axes.axis(False)
        savefig(fname, os.path.join(fpath, 'outputs'), extensions=['png'])

    if not os.path.exists(os.path.join(fpath, 'data', fname + '.npy')):
        np.save(os.path.join(fpath, 'data', fname), mm.x.dat.data)
        if not plot_mesh:
            pytest.xfail("Needed to set up the test. Please try again.")
    loaded = np.load(os.path.join(fpath, 'data', fname + '.npy'))
    assert np.allclose(mm.x.dat.data, loaded), "Mesh does not match data"
Exemple #2
0
def test_hessian_bowl(dim, interp, recovery, plot_mesh=False):
    r"""
    Check that the recovered Hessian of the quadratic
    function

  ..math::
        u(\mathbf x) = \frac12(\mathbf x \cdot \mathbf x)

    is the identity matrix.

    :arg interp: toggle whether or not to interpolate
        into :math:`\mathbb P1` space before recovering
        the Hessian.
    """
    if not interp and recovery == 'ZZ':
        pytest.xfail("Zienkiewicz-Zhu requires a Function, rather than an expression.")
    op = Options(hessian_recovery=recovery)
    n = 100 if dim == 2 else 40
    if dim == 3 and interp:
        pytest.xfail("We cannot expect recovery to be good here.")
    mesh = uniform_mesh(dim, n, 2)
    x = SpatialCoordinate(mesh)
    mesh.coordinates.interpolate(as_vector([xi - 1 for xi in x]))

    # Construct Hessian
    f = 0.5*dot(x, x)
    if interp:
        f = interpolate(f, FunctionSpace(mesh, "CG", 1))
    H = recover_hessian(f, mesh=mesh, op=op)

    # Construct analytical solution
    I = interpolate(Identity(dim), H.function_space())

    # Check correspondence
    tol = 1.0e-5 if dim == 3 else 0.8 if interp else 1.0e-6
    assert np.allclose(H.dat.data, I.dat.data, atol=tol)
    if not plot_mesh:
        return
    if dim != 2:
        raise ValueError("Cannot plot in {:d} dimensions".format(dim))

    # Plot errors on scatterplot
    H_arr = np.abs(H.dat.data - I.dat.data)
    ones = np.ones(len(H_arr))
    fig, axes = plt.subplots(figsize=(5, 6))
    for i in range(2):
        for j in range(2):
            axes.scatter((2*i + j + 1)*ones, H_arr[:, i, j], marker='x')
    axes.set_yscale('log')
    axes.set_xlim([0, 5])
    axes.set_xticks([1, 2, 3, 4])
    axes.set_xticklabels(["(0,0)", "(0,1)", "(1,0)", "(1,1)"])
    axes.set_xlabel("Hessian component")
    if interp:
        if recovery == 'ZZ':
            axes.set_yticks([1e-30, 1e-25, 1e-20, 1e-15, 1e-10, 1e-5, 1])
        else:
            axes.set_yticks([1e-15, 1e-10, 1e-5, 1])
    axes.set_ylabel("Absolute error")
    savefig("hessian_errors_bowl_{:s}".format(recovery), "outputs/hessian", extensions=["pdf"])
Exemple #3
0
def postproc_metric(d, **kwargs):
    """
    Post-process a metric field in order to enforce max/min element sizes and anisotropy.
    """
    from adapt_utils.options import Options
    op = kwargs.get('op', Options())
    return postproc_metric_str % (d*d, d, d, d, d, d, d, d, d, op.h_min, op.h_max, d, op.max_anisotropy)
Exemple #4
0
def steady_metric(f=None, H=None, projector=None, mesh=None, V=None, **kwargs):
    r"""
    Computes the steady metric for mesh adaptation.

    Clearly at least one of `f` and `H` must not be provided.

    :kwarg f: Field to compute the Hessian of.
    :kwarg H: Reconstructed Hessian associated with `f` (if already computed).
    :kwarg projector: :class:`DoubleL2Projector` object to compute Hessian.
    :kwarg normalise: toggle spatial normalisation.
    :kwarg enforce_constraints: Toggle enforcement of element size/anisotropy constraints.
    :kwarg op: :class:`Options` parameter class.
    :return: Steady metric associated with Hessian `H`.
    """
    kwargs.setdefault('normalise', True)
    kwargs.setdefault('enforce_constraints', True)
    kwargs.setdefault('noscale', False)
    kwargs.setdefault('op', Options())
    op = kwargs.get('op')
    if f is None:
        try:
            assert H is not None
        except AssertionError:
            raise ValueError("Please supply either field for recovery, or Hessian thereof.")
    elif H is None:
        H = recovery.recover_hessian(f, mesh=mesh, V=V, op=op) if projector is None else projector.project(f)
    V = V or H.function_space()
    if not hasattr(H, 'function_space'):
        H = interpolate(H, V)
    if mesh is None:
        mesh = V.mesh()
    dim = mesh.topological_dimension()
    assert dim in (2, 3)

    # Functions to hold metric and its determinant
    M = Function(V).assign(0.0)

    # Turn Hessian into a metric
    op.print_debug("METRIC: Constructing metric from Hessian...")
    kernel = eigen_kernel(metric_from_hessian, dim)
    op2.par_loop(kernel, V.node_set, M.dat(op2.RW), H.dat(op2.READ))

    # Apply Lp normalisation
    if kwargs.get('normalise'):
        op.print_debug("METRIC: Normalising metric in space...")
        noscale = kwargs.get('noscale')
        integral = kwargs.get('integral')
        space_normalise(M, noscale=noscale, f=f, integral=integral, op=op)

    # Enforce maximum/minimum element sizes and anisotropy
    if kwargs.get('enforce_constraints'):
        op.print_debug("METRIC: Enforcing elemental constraints...")
        enforce_element_constraints(M, op=op)

    # Check a valid metric
    if op.debug:
        check_spd(M)

    return M
Exemple #5
0
def isotropic_metric(f, **kwargs):
    r"""
    Given a scalar error indicator field `f`, construct an associated isotropic metric field.

    :arg f: Function to adapt to.
    :kwarg normalise: If `False` then we simply take the diagonal matrix with `f` in modulus.
    :kwarg op: :class:`Options` object providing min/max cell size values.
    :return: Isotropic metric corresponding to `f`.
    """
    kwargs.setdefault('normalise', True)
    kwargs.setdefault('enforce_constraints', True)
    kwargs.setdefault('noscale', False)
    kwargs.setdefault('op', Options())
    op = kwargs.get('op')
    try:
        assert f is not None
        assert len(f.ufl_element().value_shape()) == 0
    except AssertionError:
        raise ValueError("Provide a scalar function to compute an isotropic metric w.r.t.")
    V = f.function_space()
    mesh = V.mesh()
    V_ten = TensorFunctionSpace(mesh, V.ufl_element().family(), V.ufl_element().degree())
    dim = mesh.topological_dimension()
    assert dim in (2, 3)

    # Project into P1 space
    op.print_debug("METRIC: Constructing isotropic metric...")
    M_diag = project(max_value(abs(f), 1e-12), V)
    M_diag.interpolate(abs(M_diag))  # Ensure positivity

    # Normalise
    if kwargs.get('normalise'):
        op.print_debug("METRIC: Normalising metric...")
        assert op.normalisation in ('complexity', 'error')
        rescale = 1.0 if kwargs.get('noscale') else op.target
        p = op.norm_order
        detM = M_diag.copy(deepcopy=True)
        if p is not None:
            assert p >= 1
            detM *= M_diag
            M_diag *= pow(detM, -1/(2*p + dim))
            detM.interpolate(pow(detM, p/(2*p + dim)))
        if op.normalisation == 'complexity':
            M_diag *= pow(rescale/assemble(detM*dx), 2/dim)
        else:  # FIXME!!!
            if p is not None:
                M_diag *= pow(assemble(detM*dx), 1/p)
            M_diag *= dim/rescale

    # Enforce maximum/minimum element sizes
    if kwargs.get('enforce_constraints'):
        op.print_debug("METRIC: Enforcing elemental constraints...")
        M_diag = max_value(1/pow(op.h_max, 2), min_value(M_diag, 1/pow(op.h_min, 2)))

    # Check a valid metric
    M = interpolate(M_diag*Identity(dim), V_ten)
    if op.debug:
        check_spd(M)
    return M
Exemple #6
0
def test_combine_monitor(plot_mesh=False):
    """
    For monitor functions based on the sensors
    above, compare the meshes resulting from
    averaging and intersection (visually).

    In terms of testing, we just check for
    convergence and that volume is conserved.
    """
    alpha = Constant(10.0)  # Controls magnitude of arc
    beta = Constant(10.0)   # Controls width of arc

    def monitor1(mesh=None, x=None):
        x, y = x or SpatialCoordinate(mesh)
        return 1.0 + alpha*exp(-beta*abs(0.5 - x**2 - y**2))

    def monitor2(mesh=None, x=None):
        x, y = x or SpatialCoordinate(mesh)
        return 1.0 + alpha*exp(-beta*abs(0.5 - (1 - x)**2 - y**2))

    # Set parameters
    kwargs = {
        'approach': 'monge_ampere',
        'r_adapt_rtol': 1.0e-08,
        'nonlinear_method': 'relaxation',
    }
    op = Options(**kwargs)
    fpath = os.path.dirname(__file__)

    for mode in [1, 2, 'avg', 'int']:
        fname = {1: 'monitor1', 2: 'monitor2', 'avg': 'average', 'int': 'intersection'}[mode]
        monitor = monitor_combine(monitor1, monitor2, mode)

        # Create domain
        mesh = uniform_mesh(2, 100)
        orig_vol = assemble(Constant(1.0)*dx(domain=mesh))

        # Move the mesh and check for volume conservation
        mm = MeshMover(mesh, monitor, op=op)
        mm.adapt()
        mesh.coordinates.assign(mm.x)
        vol = assemble(Constant(1.0)*dx(domain=mesh))
        assert np.isclose(orig_vol, vol), "Volume is not conserved!"

        h = interpolate(CellSize(mesh), mm.P0).vector().gather()
        print("Combination method '{:s}':".format(fname))
        print("Minimum cell size = {:.4e}".format(h.min()))
        print("Maximum cell size = {:.4e}".format(h.max()))
        print("Mean cell size    = {:.4e}".format(np.mean(h)))

        # Plot mesh
        if plot_mesh:
            fig, axes = plt.subplots(figsize=(5, 5))
            triplot(mesh, axes=axes, interior_kw={'linewidth': 0.1}, boundary_kw={'color': 'k'})
            axes.axis(False)
            savefig(fname, os.path.join(fpath, 'outputs', op.approach), extensions=['png'])
            plt.close()
Exemple #7
0
def enforce_element_constraints(M, op=Options(), boundary_tag=None):
    """
    Post-process a metric `M` so that it obeys the following constraints specified by
    :class:`Options` class `op`:

      * `h_min`          - minimum element size;
      * `h_max`          - maximum element size;
      * `max_anisotropy` - maximum element anisotropy.
    """
    V = M.function_space()
    kernel = eigen_kernel(postproc_metric, V.mesh().topological_dimension(), op=op)
    node_set = V.node_set if boundary_tag is None else DirichletBC(V, 0, boundary_tag).node_set
    op2.par_loop(kernel, node_set, M.dat(op2.RW))
Exemple #8
0
 def __init__(self, function_space, bcs=None, op=Options(), **kwargs):
     self.op = op
     self.field = Function(function_space)
     self.mesh = function_space.mesh()
     self.dim = self.mesh.topological_dimension()
     assert self.dim in (2, 3)
     self.n = FacetNormal(self.mesh)
     self.bcs = bcs
     self.kwargs = {
         'solver_parameters':
         op.hessian_solver_parameters[op.hessian_recovery],
     }
     if op.debug:
         self.kwargs['solver_parameters']['ksp_monitor'] = None
         self.kwargs['solver_parameters']['ksp_converged_reason'] = None
Exemple #9
0
def boundary_steady_metric(H, mesh=None, boundary_tag='on_boundary', **kwargs):
    """
    Computes a boundary metric from a boundary Hessian, following the approach of
    [Loseille et al. 2011].

    :kwarg H: reconstructed boundary Hessian
    :kwarg normalise: toggle spatial normalisation.
    :kwarg enforce_constraints: Toggle enforcement of element size/anisotropy constraints.
    :kwarg op: :class:`Options` parameter class.
    :return: boundary metric associated with Hessian `H`.
    """
    kwargs.setdefault('normalise', True)
    kwargs.setdefault('enforce_constraints', True)
    kwargs.setdefault('noscale', False)
    op = kwargs.get('op', Options())
    if mesh is None:
        mesh = H.function_space().mesh()
    dim = mesh.topological_dimension()
    if dim not in (2, 3):
        raise ValueError("Dimensions other than 2D and 3D not considered.")
    P1_ten = TensorFunctionSpace(mesh, "CG", 1)
    M = Function(P1_ten, name="Boundary metric")

    # Ensure positive definite
    op.print_debug("METRIC: Ensuring positivity of boundary metric...")
    M.interpolate(abs(H))

    # Apply Lp normalisation
    if kwargs.get('normalise'):
        op.print_debug("METRIC: Normalising boundary metric in space...")
        noscale = kwargs.get('noscale')
        integral = kwargs.get('integral')
        space_normalise(M, noscale=noscale, integral=integral, boundary=True, op=op)

    # Enforce maximum/minimum element sizes and anisotropy
    if kwargs.get('enforce_constraints'):
        op.print_debug("METRIC: Enforcing elemental constraints on boundary metric...")
        enforce_element_constraints(M, boundary_tag='on_boundary', op=op)

    # Check a valid metric
    if op.debug:
        check_spd(M)

    return M
Exemple #10
0
def recover_gradient(f, **kwargs):
    r"""
    Assuming the function `f` is P1 (piecewise linear and continuous), direct differentiation will
    give a gradient which is P0 (piecewise constant and discontinuous). Since we would prefer a
    smooth gradient, we solve an auxiliary finite element problem in P1 space. This 'L2 projection'
    gradient recovery technique makes use of the Cl\'ement interpolation operator.

    That `f` is P1 is not actually a requirement. In fact, this implementation supports an argument
    which is a scalar UFL expression.

    :arg f: field which we seek the gradient of.
    :kwarg bcs: boundary conditions for L2 projection.
    :param op: `Options` class object providing min/max cell size values.
    :return: reconstructed gradient associated with `f`.
    """
    op = kwargs.get('op', Options())
    if op.gradient_recovery == 'ZZ':
        return recover_zz(f, **kwargs)

    # Argument is a Function
    if isinstance(f, Function):
        return L2ProjectorGradient(f.function_space(), **kwargs).project(f)
    op.print_debug("RECOVERY: Recovering gradient on domain interior...")
    if op.debug:
        op.gradient_solver_parameters['ksp_monitor'] = None
        op.gradient_solver_parameters['ksp_converged_reason'] = None

    # Argument is a UFL expression
    bcs = kwargs.get('bcs')
    mesh = kwargs.get('mesh', op.default_mesh)
    P1_vec = VectorFunctionSpace(mesh, "CG", 1)
    g, φ = TrialFunction(P1_vec), TestFunction(P1_vec)
    n = FacetNormal(mesh)
    a = inner(φ, g) * dx
    # L = inner(φ, grad(f))*dx
    L = f * dot(φ, n) * ds - div(φ) * f * dx  # Enables field to be P0
    l2_proj = Function(P1_vec, name="Recovered gradient")
    solve(a == L,
          l2_proj,
          bcs=bcs,
          solver_parameters=op.gradient_solver_parameters)
    return l2_proj
Exemple #11
0
def test_gradient_linear(dim, recovery):
    r"""
    Given a simple linear field :math:`f = x`, we
    check that the recovered gradient matches the
    analytical gradient, :math:`\nabla f = (1, 0)`.
    """
    op = Options(gradient_recovery=recovery)

    # Define test function
    mesh = uniform_mesh(dim, 3)
    x = SpatialCoordinate(mesh)
    P1 = FunctionSpace(mesh, "CG", 1)
    f = interpolate(x[0], P1)

    # Recover gradient using specified method
    g = recover_gradient(f, op=op)

    # Compare with analytical solution
    dfdx = np.zeros(dim)
    dfdx[0] = 1
    analytical = [dfdx for i in g.dat.data]
    assert np.allclose(g.dat.data, analytical)
Exemple #12
0
    def __init__(self, mesh, monitor_function, **kwargs):
        self.mesh = mesh
        self.dim = self.mesh.topological_dimension()
        if self.dim != 2:
            raise NotImplementedError(
                "r-adaptation only currently considered in 2D.")
        self.monitor_function = monitor_function
        self.method = kwargs.get('method', 'monge_ampere')
        if self.method != 'monge_ampere':
            raise NotImplementedError
        self.op = kwargs.get('op', Options())
        self.nonlinear_method = kwargs.get('nonlinear_method',
                                           self.op.nonlinear_method)
        assert self.nonlinear_method in ('quasi_newton', 'relaxation')
        self.bc = kwargs.get('bc')
        self.bbc = kwargs.get('bbc')
        self.ξ = Function(self.mesh.coordinates,
                          name="Computational coordinates")
        self.x = Function(self.mesh.coordinates, name="Physical coordinates")
        self.I = Identity(self.dim)
        self.pseudo_dt = Constant(self.op.pseudo_dt)

        # Create functions and solvers
        self._create_function_spaces()
        self._create_functions()
        self._initialise_sigma()
        self._setup_equidistribution()
        self._setup_l2_projector()
        self._setup_pseudotimestepper()

        # Outputs
        if self.op.debug and self.op.debug_mode == 'full':
            import os
            self.monitor_file = File(
                os.path.join(self.op.di, 'monitor_debug.pvd'))
            self.volume_file = File(
                os.path.join(self.op.di, 'volume_debug.pvd'))
        self.msg = "{:4d}   Min/Max {:10.4e}   Residual {:10.4e}   Equidistribution {:10.4e}"
Exemple #13
0
def volume_and_surface_contributions(interior_hessian, boundary_hessian, op=Options(), **kwargs):
    r"""
    Solve algebraic problem to get scaling coefficient for interior and boundary metrics to
    obtain a given metric complexity. See [Loseille et al. 2011] for details.

    :arg interior_hessian: component from domain interior.
    :arg boundary_hessian: component from domain boundary.
    :kwarg interior_hessian_scaling: positive scaling function for interior Hessian.
    :kwarg boundary_hessian_scaling: positive scaling function for boundary Hessian.
    :kwarg op: :class:`Options` parameters object.
    :return: Scaling coefficient.
    """
    from sympy import Symbol, solve

    # Collect parameters
    d = interior_hessian.function_space().mesh().topological_dimension()
    assert d in (2, 3)
    p = op.norm_order
    g = kwargs.get('interior_hessian_scaling', Constant(1.0))
    gbar = kwargs.get('boundary_hessian_scaling', Constant(1.0))
    op.print_debug("METRIC: original target complexity = {:.4e}".format(op.target))

    # Compute optimal coefficient for mixed interior-boundary Hessian
    if p is None:
        a = metric_complexity(interior_hessian, boundary=False)
        b = metric_complexity(boundary_hessian, boundary=True)
    else:
        a = assemble(pow(g, d/(2*p + d))*pow(det(interior_hessian), p/(2*p + d))*dx)
        b = assemble(pow(gbar, d/(2*p + d - 1))*pow(det(boundary_hessian), p/(2*p + d - 1))*ds)
    op.print_debug("METRIC: a = {:.4e}".format(a))
    op.print_debug("METRIC: b = {:.4e}".format(b))
    c = Symbol('c')
    # C = float(solve(a*pow(c, -d/(2*p + d)) + b*pow(c, -d/(2*p + d - 1)) - op.target, c)[0])
    C = float(solve(a*pow(c, d/2) + b*pow(c, (d-1)/2) - op.target, c)[0])  # NOTE: Differs from paper
    op.print_debug("METRIC: C = {:.4e}".format(C))
    return C
Exemple #14
0
def recover_gradient_sinusoidal(n, recovery, no_boundary, plot=False, norm_type='l2'):
    """
    Apply Zienkiewicz-Zhu and global L2 projection to recover
    the gradient of a sinusoidal function.
    """
    op = Options(gradient_recovery=recovery)
    kwargs = dict(levels=np.linspace(0, 6.4, 50), cmap='coolwarm')

    # Function of interest and its exact gradient
    func = lambda xx, yy: sin(pi*xx)*sin(pi*yy)
    gradient = lambda xx, yy: as_vector([pi*cos(pi*xx)*sin(pi*y),
                                         pi*sin(pi*xx)*cos(pi*yy)])
    # hessian = lambda xx, yy: as_matrix([[-pi*pi*func(xx, yy), pi*pi*cos(pi*xx)*cos(pi*yy)],
    #                                     [pi*pi*cos(pi*xx)*cos(pi*yy), -pi*pi*func(xx, yy)]])

    # Domain
    mesh = uniform_mesh(2, n)
    x, y = SpatialCoordinate(mesh)
    plex, offset, coordinates = make_consistent(mesh)

    # Spaces
    P1_vec = VectorFunctionSpace(mesh, "CG", 1)
    P1 = FunctionSpace(mesh, "CG", 1)
    P0 = FunctionSpace(mesh, "DG", 0)
    h = interpolate(CellSize(mesh), P0)
    nodes = None
    if no_boundary:
        nodes = set(range(len(mesh.coordinates.dat.data)))
        nodes = list(nodes.difference(set(DirichletBC(P1, 0, 'on_boundary').nodes)))

    # P1 interpolant
    u_h = interpolate(func(x, y), P1)

    # Exact gradient interpolated into P1 space
    sigma = interpolate(gradient(x, y), P1_vec)
    if norm_type == 'l2':
        sigma_norm = l2_norm(sigma.dat.data, nodes=nodes)
    else:
        assert nodes is None
        sigma_norm = norm(sigma, norm_type=norm_type)

    # Recovered gradient
    sigma_rec = recover_gradient(u_h, op=op)
    if norm_type == 'l2':
        relative_error = l2_norm(sigma.dat.data - sigma_rec.dat.data, nodes=nodes)/sigma_norm
    else:
        relative_error = errornorm(sigma, sigma_rec, norm_type=norm_type)/sigma_norm

    # Plotting
    if plot:
        fig, axes = plt.subplots(ncols=2, figsize=(11, 5))
        axes[0].set_title("Exact")
        fig.colorbar(tricontourf(sigma, axes=axes[0], **kwargs), ax=axes[0])
        triplot(mesh, axes=axes[0])
        axes[0].axis(False)
        axes[0].set_xlim([-0.1, 1.1])
        axes[0].set_ylim([-0.1, 1.1])
        axes[1].set_title("Global L2 projection" if method == 'L2' else "Zienkiewicz-Zhu")
        fig.colorbar(tricontourf(sigma_rec, axes=axes[1], **kwargs), ax=axes[1])
        triplot(mesh, axes=axes[1])
        axes[1].axis(False)
        axes[1].set_xlim([-0.1, 1.1])
        axes[1].set_ylim([-0.1, 1.1])
        plt.show()
    return h.dat.data[0], relative_error
Exemple #15
0
def recover_boundary_hessian(f, **kwargs):
    """
    Recover the Hessian of `f` on the domain boundary. That is, the Hessian in the direction
    tangential to the boundary. In two dimensions this gives a scalar field, whereas in three
    dimensions it gives a 2D field on the surface. The resulting field should only be considered on
    the boundary and is set arbitrarily to 1/h_max in the interior.

    :arg f: field which we seek the Hessian of.
    :kwarg op: `Options` class object providing max cell size value.
    :kwarg boundary_tag: physical ID for boundary segment
    :return: reconstructed boundary Hessian associated with `f`.
    """
    from adapt_utils.adapt.metric import steady_metric
    from adapt_utils.linalg import get_orthonormal_vectors

    kwargs.setdefault('op', Options())
    op = kwargs.get('op')
    op.print_debug("RECOVERY: Recovering Hessian on domain boundary...")
    mesh = kwargs.get('mesh', op.default_mesh)
    dim = mesh.topological_dimension()
    if dim not in (2, 3):
        raise ValueError("Dimensions other than 2D and 3D not considered.")

    # Solver parameters
    solver_parameters = op.hessian_solver_parameters['parts']
    if op.debug:
        solver_parameters['ksp_monitor'] = None
        solver_parameters['ksp_converged_reason'] = None

    # Function spaces
    P1 = FunctionSpace(mesh, "CG", 1)
    P1_ten = TensorFunctionSpace(mesh, "CG", 1)

    # Apply Gram-Schmidt to get tangent vectors
    n = FacetNormal(mesh)
    s = get_orthonormal_vectors(n)
    ns = as_vector([n, *s])

    # --- Solve tangent to boundary

    bcs = kwargs.get('bcs')
    assert bcs is None  # TODO
    boundary_tag = kwargs.get('boundary_tag', 'on_boundary')
    Hs, v = TrialFunction(P1), TestFunction(P1)
    l2_proj = [[Function(P1) for i in range(dim - 1)] for j in range(dim - 1)]
    h = Constant(1 / op.h_max**2)

    # Arbitrary value in domain interior
    a = v * Hs * dx
    L = v * h * dx

    # Hessian on boundary
    a_bc = v * Hs * ds
    nullspace = VectorSpaceBasis(constant=True)
    for j, s1 in enumerate(s):
        for i, s0 in enumerate(s):
            L_bc = -dot(s0, grad(v)) * dot(s1, grad(f)) * ds
            bbcs = None  # TODO?
            bcs = EquationBC(a_bc == L_bc,
                             l2_proj[i][j],
                             boundary_tag,
                             bcs=bbcs)
            solve(a == L,
                  l2_proj[i][j],
                  bcs=bcs,
                  nullspace=nullspace,
                  solver_parameters=solver_parameters)

    # --- Construct tensor field

    boundary_hessian = Function(P1_ten)
    if dim == 2:
        Hsub = abs(l2_proj[i][j])
        H = as_matrix([[h, 0], [0, Hsub]])
    else:
        Hsub = Function(TensorFunctionSpace(mesh, "CG", 1, shape=(2, 2)))
        Hsub.interpolate(
            as_matrix([[l2_proj[0][0], l2_proj[0][1]],
                       [l2_proj[1][0], l2_proj[1][1]]]))
        Hsub = steady_metric(H=Hsub,
                             normalise=False,
                             enforce_constraints=False,
                             op=op)
        H = as_matrix([[h, 0, 0], [0, Hsub[0, 0], Hsub[0, 1]],
                       [0, Hsub[1, 0], Hsub[1, 1]]])

    # Arbitrary value in domain interior
    sigma, tau = TrialFunction(P1_ten), TestFunction(P1_ten)
    a = inner(tau, sigma) * dx
    L = inner(tau, h * Identity(dim)) * dx

    # Boundary values imposed as in [Loseille et al. 2011]
    a_bc = inner(tau, sigma) * ds
    L_bc = inner(tau, dot(transpose(ns), dot(H, ns))) * ds
    bcs = EquationBC(a_bc == L_bc, boundary_hessian, boundary_tag)
    solve(a == L,
          boundary_hessian,
          bcs=bcs,
          solver_parameters=solver_parameters)
    return boundary_hessian
Exemple #16
0
def cell_size_metric(mesh, op=Options()):
    P1 = FunctionSpace(mesh, "CG", 1)
    return isotropic_metric(interpolate(pow(CellSize(mesh), -2), P1), normalise=False, op=op)
def test_sensors(sensor, monitor_type, method, plot_mesh=False):
    alpha = Constant(5.0)
    hessian_kwargs = dict(enforce_constraints=False,
                          normalise=True,
                          noscale=True)
    pytest.xfail("FIXME")  # FIXME

    def shift_and_scale(mesh=None, x=None):
        """
        Adapt to the scaled and shifted sensor magnitude.
        """
        f = interpolate(abs(sensor(mesh, xy=x)), FunctionSpace(mesh, "CG", 1))
        return 1.0 + alpha * abs(sensor(mesh,
                                        xy=x)) / f.vector().gather().max()

    def gradient(mesh=None, x=None):
        """
        Adapt to a recovered gradient for the sensor.
        """
        g = recover_gradient(sensor(mesh, xy=x), mesh=mesh, op=op)
        gmax = g.vector().gather().max()
        return 1.0 + alpha * dot(g, g) / dot(gmax, gmax)

    def frobenius(mesh=None, x=None):
        """
        Adapt to the Frobenius norm of an L1-normalised
        Hessian metric for the sensor.
        """
        P1 = FunctionSpace(mesh, "CG", 1)
        M = steady_metric(sensor(mesh, xy=x),
                          mesh=mesh,
                          op=op,
                          **hessian_kwargs)
        M_F = local_frobenius_norm(M, space=P1)
        return 1.0 + alpha * M_F / interpolate(M_F, P1).vector().gather().max()

    def density(mesh=None, x=None):
        """
        Adapt to the density of an L1-normalised
        Hessian metric for the sensor.
        """
        M = steady_metric(sensor(mesh, xy=x),
                          mesh=mesh,
                          op=op,
                          **hessian_kwargs)
        rho = get_density_and_quotients(M)[0]
        return 1.0 + alpha * rho / rho.vector().gather().max()

    rtol = 1.0e-03
    if monitor_type == 'shift_and_scale':
        monitor = shift_and_scale
    elif monitor_type == 'gradient':
        monitor = gradient
    elif monitor_type == 'frobenius':
        monitor = frobenius
    elif monitor_type == 'density':
        monitor = density
    else:
        raise ValueError(
            "Monitor function type {:s} not recognised.".format(monitor_type))

    # Set parameters
    kwargs = {
        'approach': 'monge_ampere',
        'r_adapt_rtol': rtol,
        'nonlinear_method': method,
        'normalisation': 'complexity',
        'norm_order': None,
    }
    op = Options(**kwargs)
    fname = '_'.join([sensor.__name__, monitor_type, method])
    fpath = os.path.dirname(__file__)

    # Create domain
    n = 100
    mesh = SquareMesh(n, n, 2)
    x, y = SpatialCoordinate(mesh)
    mesh.coordinates.interpolate(as_vector([x - 1, y - 1]))
    orig_vol = assemble(Constant(1.0) * dx(domain=mesh))

    # Move the mesh and check for volume conservation
    mm = MeshMover(mesh, monitor, op=op)
    mm.adapt()
    mesh.coordinates.assign(mm.x)
    vol = assemble(Constant(1.0) * dx(domain=mesh))
    assert np.isclose(orig_vol, vol), "Volume is not conserved!"

    # Plot mesh
    if plot_mesh:
        import matplotlib.pyplot as plt
        fig, axes = plt.subplots(figsize=(5, 5))
        triplot(mesh,
                axes=axes,
                interior_kw={'linewidth': 0.1},
                boundary_kw={'color': 'k'})
        axes.axis(False)
        savefig(fname,
                os.path.join(fpath, 'outputs', op.approach),
                extensions=['png'])
        plt.close()
        return

    # Save mesh coordinates to file
    if not os.path.exists(os.path.join(fpath, 'data', fname + '.npy')):
        np.save(os.path.join(fpath, 'data', fname), mm.x.dat.data)
        if not plot_mesh:
            pytest.xfail("Needed to set up the test. Please try again.")
    loaded = np.load(os.path.join(fpath, 'data', fname + '.npy'))
    assert np.allclose(mm.x.dat.data, loaded), "Mesh does not match data"
Exemple #18
0
def space_normalise(M, f=None, integral=None, boundary=False, **kwargs):
    r"""
    Normalise steady metric `M` based on field `f`.

    Four normalisation approaches are implemented. These include two 'straightforward' approaches:

      * 'norm'       - simply divide by the norm of `f`;
      * 'abs'        - divide by the maximum of `abs(f)` and some minimum tolerated value (see
                       equation (18) of [1]);

    and two :math:`L^p` normalisation approaches, for :math:`p\geq1` or :math:`p=\infty`:

      * 'complexity' - Lp normalisation with a target metric complexity (see equation (2.10) of [2]);
      * 'error'      - Lp normalisation with a target interpolation error (see equation (7) of [3]).

    NOTE: In the case of 'error' normalisation, 'target' is the inverse of error.


    [1] Pain et al. "Tetrahedral mesh optimisation and adaptivity for steady-state and transient
        finite element calculations", Comput. Methods Appl. Mech. Engrg. 190 (2001) pp.3771-3796.

    [2] Loseille & Alauzet, "Continuous mesh framework part II: validations and applications",
        SIAM Numer. Anal. 49 (2011) pp.61-86.

    [3] Alauzet & Olivier, "An L∞-Lp space-time anisotropic mesh adaptation strategy for
        time-dependent problems", V European Conference on Computational Fluid Dynamics (2010).
    """
    kwargs.setdefault('op', Options())
    kwargs.setdefault('f_min', 1.0e-08)
    kwargs.setdefault('noscale', False)
    op = kwargs.get('op')

    # --- Simple normalisation approaches

    if op.normalisation in ('norm', 'abs'):
        assert f is not None
        tol = kwargs.get('f_min')
        denominator = norm(f) if op.normalisation == 'norm' else max_value(abs(f), tol)
        M.interpolate(M/denominator)
        return

    # --- Lp normalisation

    if op.normalisation not in ('complexity', 'error'):
        raise ValueError("Normalisation approach {:s} not recognised.".format(op.normalisation))
    p = op.norm_order
    mesh = M.function_space().mesh()
    d = mesh.topological_dimension()
    if boundary:
        d -= 1

    if integral is None:
        target = 1 if kwargs.get('noscale') else op.target
        form = pow(det(M), 0.5) if p is None else pow(det(M), p/(2*p + d))
        integral = assemble(form*ds) if boundary else assemble(form*dx)
        if op.normalisation == 'complexity':
            integral = pow(target/integral, 2/d)
        else:
            integral = 1 if p is None else pow(integral, 1/p)
            integral *= d*target
    assert integral > 1.0e-08
    determinant = 1 if p is None else pow(det(M), -1/(2*p + d))
    M.interpolate(integral*determinant*M)
Exemple #19
0
def recover_hessian(f, **kwargs):
    r"""
    Assuming the smooth solution field has been approximated by a function `f` which is P1, all
    second derivative information has been lost. As such, the Hessian of `f` cannot be directly
    computed. We provide two means of recovering it, as follows.

    That `f` is P1 is not actually a requirement. In fact, this implementation supports an argument
    which is a scalar UFL expression.

    (1) "Integration by parts" ('parts'):
    This involves solving the PDE $H = \nabla^T\nabla f$ in the weak sense. Code is based on the
    Monge-Amp\`ere tutorial provided on the Firedrake website:
    https://firedrakeproject.org/demos/ma-demo.py.html.

    (2) "Double L2 projection" ('L2'):
    This involves two applications of the L2 projection operator. In this mode, we are permitted
    to recover the Hessian of a P0 field, since no derivatives of `f` are required.

    :arg f: field which we seek the Hessian of.
    :param op: `Options` class object providing min/max cell size values.
    :return: reconstructed Hessian associated with `f`.
    """
    op = kwargs.get('op', Options())
    if op.hessian_recovery == 'ZZ':
        op.print_debug("RECOVERY: Recovering gradient...")
        g = recover_zz(f, **kwargs)
        op.print_debug("RECOVERY: Recovering Hessian from gradient...")
        return recover_zz(g, **kwargs)

    # Argument  is a Function
    if isinstance(f, Function):
        return DoubleL2ProjectorHessian(f.function_space(),
                                        boundary=False,
                                        **kwargs).project(f)
    op.print_debug("RECOVERY: Recovering Hessian on domain interior...")

    # Argument is a UFL expression
    bcs = kwargs.get('bcs')
    mesh = kwargs.get('mesh', op.default_mesh)
    if kwargs.get('V') is not None:
        mesh = kwargs.get('V').mesh()
    P1_ten = kwargs.get('V') or TensorFunctionSpace(mesh, "CG", 1)
    n = FacetNormal(mesh)
    solver_parameters = op.hessian_solver_parameters[op.hessian_recovery]
    if op.debug:
        solver_parameters['ksp_monitor'] = None
        solver_parameters['ksp_converged_reason'] = None

    # Integration by parts
    if op.hessian_recovery == 'parts':
        H, τ = TrialFunction(P1_ten), TestFunction(P1_ten)
        l2_proj = Function(P1_ten, name="Recovered Hessian")
        a = inner(τ, H) * dx
        L = -inner(div(τ), grad(f)) * dx
        L += dot(grad(f), dot(τ, n)) * ds
        solve(a == L, l2_proj, bcs=bcs, solver_parameters=solver_parameters)
        return l2_proj

    # Double L2 projection
    P1_vec = VectorFunctionSpace(mesh, "CG", 1)
    W = P1_vec * P1_ten
    g, H = TrialFunctions(W)
    φ, τ = TestFunctions(W)
    l2_proj = Function(W)
    a = inner(τ, H) * dx
    a += inner(φ, g) * dx
    a += inner(div(τ), g) * dx
    a += -dot(g, dot(τ, n)) * ds
    # L = inner(grad(f), φ)*dx
    L = f * dot(φ, n) * ds - f * div(φ) * dx  # Enables field to be P0
    solve(a == L, l2_proj, bcs=bcs, solver_parameters=solver_parameters)
    g, H = l2_proj.split()
    H.rename("Recovered Hessian")
    return H
Exemple #20
0
def test_combine_metric(plot_mesh=False):
    """
    The complexity of the metric intersection
    should be greater than or equal to the
    complexity of the constituent metrics.

    The same can be said for the number of
    elements and vertices in the resulting
    mesh. We observe that the number of
    elements and vertices in the metric
    average is usually lower.
    """
    try:
        from firedrake import adapt
    except ImportError:
        pytest.xfail("Need Pragmatic")
    kwargs = {
        'approach': 'hessian',
        'max_adapt': 4,
        'normalisation': 'complexity',
        'norm_order': 1,
        'target': 200.0,
        'h_min': 1.0e-06,
        'h_max': 1.0,
    }
    op = Options(**kwargs)
    kwargs = {
        'op': op,
        'enforce_constraints': True,
    }

    # Loop over different metric construction modes
    data = {1: {}, 2: {}, 'avg': {}, 'int': {}}
    for mode in data:

        # Create domain [0, 1]²
        mesh = uniform_mesh(2, 100)
        for i in range(op.max_adapt):
            x, y = SpatialCoordinate(mesh)
            P1_ten = TensorFunctionSpace(mesh, "CG", 1)

            # Create a metric focused around the arc x² + y² = ½
            f = exp(-abs(0.5 - x**2 - y**2))
            M1 = steady_metric(f, V=P1_ten, **kwargs)

            # Create a metric focused around the arc (1 - x)² + y² = ½
            g = exp(-abs(0.5 - (1 - x)**2 - y**2))
            M2 = steady_metric(g, V=P1_ten, **kwargs)

            # Choose metric according to mode
            M = metric_combine(M1, M2, mode)

            # Adapt mesh
            with pipes() as (out, err):
                mesh = adapt(mesh, M)

        # Store entity counts
        data[mode]['complexity'] = metric_complexity(M)
        data[mode]['num_cells'] = mesh.num_cells()
        data[mode]['num_vertices'] = mesh.num_vertices()

        # Plot
        if plot_mesh:
            fig, axes = plt.subplots(figsize=(5, 5))
            triplot(mesh, axes=axes, interior_kw={'linewidth': 0.1}, boundary_kw={'color': 'k'})
            axes.axis(False)
            savefig('mesh_{:}'.format(mode), 'outputs/hessian', extensions=['png'])

    # Check complexity follows C(M1) ≤ C(M1 ∩ M2) and similarly for M2
    msg = "M_{:d} does not have {:s} {:s} than M_{:s}"
    i = 'complexity'
    for j in (1, 2):
        assert data[j][i] <= data['int'][i], msg.format(j, 'fewer', i, 'int')

    # Similarly for element counts, also with lower bound
    for i in ('num_cells', 'num_vertices'):
        for j in (1, 2):
            assert data['avg'][i] <= data[j][i], msg.format(j, 'more', i, 'avg')
            assert data[j][i] <= data['int'][i], msg.format(j, 'fewer', i, 'int')

    # Print mesh data to screen
    if plot_mesh:
        for mode in data:
            print("Mode M_{:}".format(mode))
            print("number of elements = {:d}".format(data[mode]['num_cells']))
            print("number of vertices = {:d}".format(data[mode]['num_vertices']))
Exemple #21
0
def space_time_normalise(hessians, timestep_integrals=None, enforce_constraints=True, op=Options()):
    r"""
    Normalise a list of Hessians in space and time as dictated by equation (1) in
    [Barral et al. 2016].

    :arg hessians: list of Hessians to be time-normalised.
    :kwarg timestep_integrals: list of time integrals of 1/timestep over each remesh step.
        For constant timesteps, this equates to the number of timesteps per remesh step.
    :kwarg op: :class:`Options` object providing desired average instantaneous metric complexity.
    """
    p = op.norm_order
    if p is not None:
        assert p >= 1
    n = len(hessians)
    timestep_integrals = timestep_integrals or np.ones(n)*np.floor(op.end_time/op.dt)/op.num_meshes
    assert len(timestep_integrals) == n
    d = hessians[0].function_space().mesh().topological_dimension()

    # Target space-time complexity
    N_st = op.target*sum(timestep_integrals)  # Multiply instantaneous target by number of timesteps

    # Compute global normalisation coefficient
    op.print_debug("METRIC: Computing global metric time normalisation factor...")
    integral = 0.0
    for i, H in enumerate(hessians):  # TODO: Check whether square is correct
        if p is None:
            integral += assemble(sqrt(det(H))*timestep_integrals[i]*dx)
        else:
            integral += assemble(pow(det(H)*timestep_integrals[i]**2, p/(2*p + d))*dx)
    if op.normalisation == 'complexity':
        glob_norm = pow(N_st/integral, 2/d)
    elif op.normalisation == 'error':  # FIXME
        glob_norm = d*N_st
        # glob_norm = d*op.target
        if p is not None:
            glob_norm *= pow(integral, 1/p)
    else:
        raise ValueError("Normalisation approach {:s} not recognised.".format(op.normalisation))
    op.print_debug("METRIC: Target space-time complexity = {:.4e}".format(N_st))
    op.print_debug("METRIC: Global normalisation factor = {:.4e}".format(glob_norm))

    # Normalise on each window
    op.print_debug("METRIC: Normalising metric in time...")
    for i, H in enumerate(hessians):
        H_expr = glob_norm*H
        if p is not None:
            H_expr *= pow(det(H)*timestep_integrals[i]**2, -1/(2*p + d))
        H.interpolate(H_expr)
        H.rename("Time-accurate {:s}".format(H.dat.name))

        # Enforce max/min element sizes and anisotropy
        if enforce_constraints:
            op.print_debug("METRIC: Enforcing size and ansisotropy constraints...")
            enforce_element_constraints(H, op=op)