Examples and tutorials » Mesh geometry examples » Computing arrival times with fast marching

Propagate from one exact seed, compare with an analytic distance and check refinement.

Arrival time and speed

A front propagates through the unit square with positive speed $F(x)$ . The arrival time $T$ satisfies

\[ F(x)\lVert\nabla T(x)\rVert=1, \qquad T(c)=0,\qquad c=(0.5,0.5). \]

For constant speed $F=s$ , the exact solution is

\[ T(x)=\frac{\lVert x-c\rVert}{s}. \]

Unit speed produces distance; doubling the speed halves every arrival time. This provides a direct check of the speed convention used by FMM.

Choose an exact seed

The program uses triangular grids with 16, 32 and 64 intervals per axis. Each count is even, so the center is a mesh vertex at every refinement. Seeding a neighborhood around the center would prescribe zero time on a larger source set and change the reference problem.

      const size_t n = intervals + 1;
      const Real h = Real(1) / intervals;
      auto mesh = LocalMesh::UniformGrid(Polytope::Type::Triangle, {n, n});
      mesh.scale(h);
      mesh.getConnectivity().compute(2, 0);
      mesh.getConnectivity().compute(0, 2);
      mesh.getConnectivity().compute(0, 0);

Vertex-to-cell incidence supplies the local simplicial stencils. Vertex-to-vertex incidence supplies neighboring trial vertices. Both are prepared before seeding and propagation.

Propagate the front

A scalar continuous $P_1$ field stores nodal arrival times. The source contains exactly one vertex. seed() sets its value to zero and initializes neighboring trial values; solve() repeatedly accepts the smallest trial arrival time and updates its neighbors.

      P1 space(mesh);
      GridFunction arrival(space);
      IndexVector seeds;
      for (auto vertex = mesh.getVertex(); vertex; ++vertex)
      {
        const auto& x = vertex->getCoordinates();
        if (x(0) == Real(0.5) && x(1) == Real(0.5))
          seeds.push_back(vertex->getIndex());
      }
      if (seeds.size() != 1)
        return EXIT_FAILURE;
      const auto speed = [propagationSpeed](const Point&) { return propagationSpeed; };
      Eikonal::FMM marching(arrival, speed);
      marching.seed(seeds).solve();

The callable returns propagation speed in distance per unit time. Supply finite, strictly positive values. Its reciprocal is the local slowness. This example uses constant speeds 1 and 2, with identical grids and seeds.

Compare with the analytic field

Image
Computed unit-speed arrival time on the 64-interval grid. Contours show equal arrival times. The dense triangle edges are omitted so that they do not obscure the field.

For the nodal set $\mathcal V_h$ , the program records

\[ e_\infty=\max_{a\in\mathcal V_h}|T_h(a)-T(a)|, \qquad e_{\mathrm{mean}}=\frac{1}{|\mathcal V_h|} \sum_{a\in\mathcal V_h}|T_h(a)-T(a)|. \]

These are nodal error measures. The arithmetic mean is independent of cell areas and differs from an integrated error norm.

      Real maximum = 0, mean = 0;
      for (auto vertex = mesh.getVertex(); vertex; ++vertex)
      {
        const auto& x = vertex->getCoordinates();
        const auto& dofs = space.getDOFs(0, vertex->getIndex());
        const Real computed = arrival.getData()(dofs[0]);
        const Real exact =
          std::hypot(x(0) - Real(0.5), x(1) - Real(0.5)) / propagationSpeed;
        const Real error = std::abs(computed - exact);
        valid = valid && std::isfinite(computed) && computed >= 0;
        maximum = std::max(maximum, error);
        mean += error;
      }
      mean /= mesh.getVertexCount();
      // A deliberately loose O(h) regression envelope on this mesh sequence.
      // Measured errors, rather than this bound, characterize the observed rate.
      valid =
        valid && maximum < previousError && maximum <= Real(2) * h / propagationSpeed;
      previousError = maximum;
      errors << intervals << ',' << propagationSpeed << ',' << maximum << ',' << mean
             << '\n';

The checked local results for unit speed are:

Intervals per axisVerticesMaximum nodal errorMean nodal error
16289 $0.0474507$ $0.0158231$
321089 $0.0299216$ $0.0102179$
644225 $0.0182986$ $0.00634955$

Both errors decrease under refinement. With speed 2, every tabulated error is halved to printed precision. The point-source solution has a singular gradient at the center; this sequence supplies measured convergence for this setup, without assuming the interpolation order of a smooth field.

The executable requires finite nonnegative arrival times, a unique source, strictly decreasing maximum errors and the loose regression envelope $e_\infty\leq 2h/s$ . That envelope is a check for this example's grids. Assess other geometries and variable-speed fields against their own reference solutions or refinement studies.

Build and inspect

Build the Eikonal_FMM target and run it in a scratch directory. It needs no input mesh. FMMErrors.csv contains all six refinement/speed results. FMMArrival.xdmf and its HDF5 companions contain the finest unit-speed computed field arrival and interpolated analytic nodal field reference. Open the XDMF in ParaView and select Contour on arrival to inspect the advancing fronts.

Complete program

The complete program is available in FMM.cpp.

Distance fields in applications

A zero-time vertex set defines an unsigned arrival-time problem. Signed level-set reconstruction additionally needs an inside/outside convention. The shape optimization guide describes transport and redistancing; Adapting and fitting an implicit interface shows how an analytic signed distance can drive mesh-interface fitting.