In this tutorial, we show how to create grid-structured domains and connect them. Grid-structured data is especially important for simulations. Grids also offer special opportunities for optimization by exploiting the regular addressing to eliminate memory accesses. Consequently, Ebb provides special mechanisms for indicating that certain relations are grid-structured, and for connecting those relations to themselves and other gridded relations.
To illustrate these features, we’re going to write two coupled simulations on grids of different scales. The lower-resolution grid will simulate a heat diffusion, while the higher-resolution grid will simulate the wave equation. The particular simulation isn’t physically derived, but will show both how to write code for grids and how to construct multi-grid structures.
We’ll start the program in the usual way.
Instead of declaring the size of a relation, we can specify dims, a Lua list of 2 or 3 numbers, giving the number of grid entries we want in each dimension. If we want the grid relation to be considered periodic, then we can additionally specify a periodic parameter. Notice that raw grid structured relations do not need an origin or width specified. Those are parameters of the standard library grid, which provides a set of standard functionality on top of the raw grid relations.
Here we define the necessary simulation variables. Rather than explicitly encode velocity, we choose to instead store the previous field value. By copying, we effectively choose to initialize everything with 0 velocity.
This computation doesn’t accomplish anything for our simulation, but it does demonstrate how we can use the special L.Affine(...) function to access neighboring elements in a grid. The first argument to L.Affine() specifies which grid-structured relation we’re performing a lookup into. The second argument specifies an affine transformation of the third argument’s “coordinates.” (since the third argument is an key from some grid strucured relation) This second argument must be a constant matrix, which we can interpret as follows: Let out be the key returned from the L.Affine call and in be the third argument input key. (here c) Then, abusing notation slightly out.x = 1 * in.x + 0 * in.y + (-1) and out.y = 0 * in.x + 1 * in.y + 0. That is, left_c is just the cell displaced by -1 in the x-direction.
Usually, we won’t type out the entire call to L.Affine. Instead we’ll use macros. (introduced in tutorial 15) Ebb provides a special syntax overloading feature when a macro is installed with the name '__apply_macro'. In this case, “function calls” to keys from the relation are redirected to the macro, which is supplied with the arguments as additional parameters. This syntax is especially valuable for giving a shorthand for relative offsets in grid data.
The second macro we define here lets us access the higher-resolution grid from the lower resolution grid. Using this connection, we can define routines to down-sample the current t-field, and also to up-sample and apply the diffusion results. Notice how various access macros can be chained together. We first access the high-resolution grid with c.hi, but then can immediately use the offset macro to locally navigate to the other 3 cells covered by the low resolution cell.
Now, we define the simulaiton at each resolution.
To define the visualization function, we use the debugging functions L.xid() and L.yid() which recover the numeric ids identifying which specific cell we’re in. We also define some debugging stats for the console. In particular, we expect that since we defined our simulation carefully, we should preserve the sum of t and see a gradual decrease in the gradient as the diffusion behavior eventually dominates.
Our simulation loop down-samples the field, runs both simulations, and then up-samples the diffusion results to combine with the wave simulation step. Then we cycle our buffers, visualize and collect statistics.