Part of what makes Ebb unique is the ability to handle simulations over very different domains. In the previous tutorials, other than the hello world, we used a triangle mesh domain. In this tutorial, we look at how to make use of the standard grid library.
import 'ebb'
local L = require 'ebblib'
local GridLib = require 'ebb.domains.grid'
local vdb = require 'ebb.lib.vdb'
We start the program more or less identically, except we pull in the grid domain library instead of the OFF loader wrapper
local N = 40
local grid = GridLib.NewGrid2d {
size = {N, N},
origin = {-N/2, -N/2},
width = {N, N},
periodic_boundary = {true,false},
}
Now, instead of loading the domain from a file, we can simply create the grid by specifying a set of named arguments. For a 2d grid, these are:
- size is the discrete dimensions of the grid; i.e. how many cells there are in the x and y directions. (here it is 40x40)
- origin is the spatial position of the (0,0) corner of the grid. (here we put the center of the grid at coordinate (0,0))
- width is the spatial dimensions of the grid (which we just put in 1-to-1 correspondence with the discrete dimension here)
- periodic_boundary is a list of flags indicating whether the given dimension should “wrap-around” or be treated as a hard boundary. (This argument is assumed to be {false,false} if not supplied) (Here, we set only the x direction to wrap around)
local timestep = L.Constant(L.double, 0.45)
local conduction = L.Constant(L.double, 1.0)
local max_diff = L.Global(L.double, 0.0)
grid.cells:NewField('t', L.double):Load(0)
grid.cells:NewField('new_t', L.double):Load(0)
local function init_temperature(x_idx, y_idx)
if x_idx == 4 and y_idx == 6 then return 1000
else return 0 end
end
grid.cells.t:Load(init_temperature)
Our definition of simulation quantities is more or less the same as for the triangle mesh. The one difference to remark on is that we define fields over grid.cells
instead of mesh.vertices
.
local ebb visualize ( c : grid.cells )
vdb.color({ 0.5 * c.t + 0.5, 0.5-c.t, 0.5-c.t })
var p2 = c.center
vdb.point({ p2[0], p2[1], 0 })
end
Unsurprisingly, this means that the visualization code is also similar. However, because cells have spatial extent, it doesn’t make sense to simply look up their position. Instead, we ask for their c.center
coordinates. (Warning: center is not actually a field, so if you try to write to it, e.g. c.center = {3,4}
you’ll get an error)
local ebb update_temperature ( c : grid.cells )
var avg = (1.0/4.0) * ( c(1,0).t + c(-1,0).t + c(0,1).t + c(0,-1).t )
var diff = avg - c.t
c.new_t = c.t + timestep * conduction * diff
end
Our new temperature update step is similar to the double-buffer strategy from Tutorial 06, but we use the special grid mechanism for neighbor access. Here, we get the temperature from the four neighbors of a cell in the positive and negative x and y directions. In general, we can get the cell offset from the current one by (x,y) by writing c(x,y)
and then access any fields on that cell.
local ebb measure_max_diff ( c : grid.cells )
var avg = (1.0/4.0) * ( c(1,0).t + c(-1,0).t + c(0,1).t + c(0,-1).t )
var diff = avg - c.t
max_diff max= L.fabs(diff)
end
The function to measure the maximum difference is essentially the same.
local ebb update_temp_boundaries ( c : grid.cells )
var avg : L.double
if c.yneg_depth > 0 then
avg = (1.0/4.0) * ( c(1,0).t + c(-1,0).t + c(0,1).t + c(0,0).t )
elseif c.ypos_depth > 0 then
avg = (1.0/4.0) * ( c(1,0).t + c(-1,0).t + c(0,0).t + c(0,-1).t )
end
var diff = avg - c.t
c.new_t = c.t + timestep * conduction * diff
end
However, simply applying update_temperature
is insufficient to handle the non-periodic boundaries in the y-direction. Instead, we’re going to need to somehow impose a boundary condition on our simulation.
By default, a non-periodic direction marks one final layer of cells on each side of the grid as part of the boundary. Then we can query how deep we are into a particular boundary using c.yneg_depth
and c.ypos_depth
. (Note that like c.center
these “fields” can’t be assigned to)
for i=1,360 do
grid.cells.interior:foreach(update_temperature)
grid.cells.boundary:foreach(update_temp_boundaries)
grid.cells:Swap('t', 'new_t')
vdb.vbegin()
vdb.frame()
grid.cells:foreach(visualize)
vdb.vend()
if i % 10 == 0 then -- measure statistics every 10 steps
max_diff:set(0)
grid.cells.interior:foreach(measure_max_diff)
print( 'iteration #'..tostring(i), 'max gradient: ', max_diff:get() )
end
end
Our simulation loop is mostly the same, but with one major difference. Rather than run update_temperature
for each cell, we only run it for each interior
cell. Likewise, we then execute the boundary computation only for each boundary cell. Though we still visualize all the cells with a single call. (Note that if we ran update_temperature
on all of the cells, then we would produce array out of bound errors.)