evaluate_dofs for expressions is sub-optimal for vector, tensor or mixed function spaces

Issue #52 open
Simone Pezzuto created an issue

Interpolating an Expression is currently sub-optimal for two reasons:

  • it iterates over the cells and the over the dofs (by restricting it), but for Lagrange-type basis function it would be more convenient to iterate over the dofs;

  • for vector, tensor, mixed function spaces, it recompute the same function (same cell and coordinates) for each component, when the result is clearly the same.

The first point is DOLFIN-related, but the second is due to FFC.

Here an example:

from fenics import *

code = \
"""\
class MyExpression : public Expression
{
public:
    MyExpression() : Expression(2) {};

    void eval(dolfin::Array<double>&,
              const dolfin::Array<double>&) const
    {
        std::cout << "call" << std::endl;
    }
};
"""

mesh = UnitTriangleMesh()
V = VectorFunctionSpace(mesh, "P", 1)
e = Expression(cppcode = code, element = V.ufl_element())
interpolate(e, V)

From the output you can easily count 6 "call"s (1 cell x 6 dofs). The generated code for the vector space overloads the method ufc::evaluate_dofs as follows:

  /// Evaluate linear functionals for all dofs on the function f
  virtual void evaluate_dofs(double* values,
                             const ufc::function& f,
                             const double* vertex_coordinates,
                             int cell_orientation,
                             const ufc::cell& c) const
  {
    // Declare variables for result of evaluation
    double vals[2];

    // Declare variable for physical coordinates
    double y[2];
    y[0] = vertex_coordinates[0];
    y[1] = vertex_coordinates[1];
    f.evaluate(vals, y, c);
    values[0] = vals[0];
    y[0] = vertex_coordinates[2];
    y[1] = vertex_coordinates[3];
    f.evaluate(vals, y, c);
    values[1] = vals[0];
    y[0] = vertex_coordinates[4];
    y[1] = vertex_coordinates[5];
    f.evaluate(vals, y, c);
    values[2] = vals[0];
    y[0] = vertex_coordinates[0];
    y[1] = vertex_coordinates[1];
    f.evaluate(vals, y, c);
    values[3] = vals[1];
    y[0] = vertex_coordinates[2];
    y[1] = vertex_coordinates[3];
    f.evaluate(vals, y, c);
    values[4] = vals[1];
    y[0] = vertex_coordinates[4];
    y[1] = vertex_coordinates[5];
    f.evaluate(vals, y, c);
    values[5] = vals[1];
  }

can be rewritten as

  /// Evaluate linear functionals for all dofs on the function f
  virtual void evaluate_dofs(double* values,
                             const ufc::function& f,
                             const double* vertex_coordinates,
                             int cell_orientation,
                             const ufc::cell& c) const
  {
    // Declare variables for result of evaluation
    double vals[2];

    // Declare variable for physical coordinates
    double y[2];
    y[0] = vertex_coordinates[0];
    y[1] = vertex_coordinates[1];
    f.evaluate(vals, y, c);
    values[0] = vals[0];
    values[3] = vals[1];
    y[0] = vertex_coordinates[2];
    y[1] = vertex_coordinates[3];
    f.evaluate(vals, y, c);
    values[1] = vals[0];
    values[4] = vals[1];
    y[0] = vertex_coordinates[4];
    y[1] = vertex_coordinates[5];
    f.evaluate(vals, y, c);
    values[2] = vals[0];
    values[5] = vals[1];
  }

Here we save half of the cost of calling eval, but for tensor-valued functions in 3d the cost would be 1/9th.

Comments (22)

  1. Jan Blechta

    If ufc::function::evaluate was marked as pure (is there the concept in C++?), compiler might be able to do this optimization.

  2. Jan Blechta
    • changed status to open

    I believe I've seen few weeks ago when debugging something else, that the factorization of common eval code does not work in non-affine cases (manifolds or higher value ranks). There's some geometry transformation code which differs for components.

  3. Martin Sandve Alnæs

    None of the element generation code works for non-affine meshes. I started work on that last year (...) and want to finish it soon. There are a couple of issues touching the subject, see:

    https://bitbucket.org/fenics-project/ffc/issues/87/evaluate_basis-error-for-combination-mixed

    https://bitbucket.org/fenics-project/ffc/issues/83/add-a-ufc-coordinate_mapping-class-was-ufc

    https://bitbucket.org/fenics-project/ffc/issues/93/replace-code-generation-for-ufc-classes

  4. Jan Blechta

    By "non-affine cases" I meant mapping != affine, i.e. Piola transformed elements. Example element = VectorElement("Raviart-Thomas", triangle, 1) emits

        y[0] = 0.5*coordinate_dofs[2] + 0.5*coordinate_dofs[4];
        y[1] = 0.5*coordinate_dofs[3] + 0.5*coordinate_dofs[5];
        f.evaluate(vals, y, c);
        result = (detJ*(K[0]*vals[0] + K[1]*vals[1])) + (detJ*(K[2]*vals[0] + K[3]*vals[1]));
        values[0] = result;
    [...]
        y[0] = 0.5*coordinate_dofs[2] + 0.5*coordinate_dofs[4];
        y[1] = 0.5*coordinate_dofs[3] + 0.5*coordinate_dofs[5];
        f.evaluate(vals, y, c);
        result = (detJ*(K[0]*vals[2] + K[1]*vals[3])) + (detJ*(K[2]*vals[2] + K[3]*vals[3]));
        values[3] = result;
    

    The first three lines of both blocks should be factored out but it does not work because first four lines are considered.

  5. Jan Blechta

    Fix for non-affine mapped elements in master by 3d57ab0b63fb50cf83fd0d47a80f467f50138074. This fixes cases like element = VectorElement("Raviart-Thomas", triangle, 1).

    Remaining is to fix the codepath going through ffc.evaluatedof._generate_multiple_points_body. This applies typically to elements with quadrature moments. Reproduce by element = FiniteElement("Brezzi-Douglas-Marini", triangle, 2). This needs some refactoring in evaluatedof.py.

  6. David

    In the meanwhile, is it possible to work around this bug? I got a problem with a code where interpolating a 3x3 tensor expression takes 3 hours due to this issue, almost half of the total run time. I'd like to avoid resorting to an older version of fenics.

  7. Log in to comment