Common implementation patterns¶
This section of the developers guide covers common patters of code used in HartreeParticleDSL, and how they work.
Data copies between Regions¶
In many places in HartreeParticleDSL, data is copied from multiple regions, which may be constructured from different field spaces. Example uses of this functionality is in the HDF5 simple IO module:
[mapper_fields:map(function(field)
return rquote
particle_array[i].[field.part_field] = hdfreg[i].[field.io_field]
end
end)];
or in the tradequeue implementation in neighbour search:
[part_structure:map(function(element)
return rquote
tradequeue[int1d(tradequeue_added)].[element.field] = parts[int1d(part)].[element.field]
end
end)];
These implementations make use of Terra’s list functionality, some of the utility functions defined in HartreeParticleDSL, and some other code to achieve automatic generation of data copies between fields.
In this section, we’re going to look explicitly at copying from two regions of the same field space, as used in the tradequeue
implementation. The mapping between regions of different field spaces is similar, but an extra mapping needs to be defined between
the field spaces (as shown in the HDF5 example above, as field.part_field and field.io_field map to the corresponding fields
in the distinct field spaces).
Setup¶
These patterns all start by creating a terralib.newlist(), and filling it with a list of fields we want to copy data to and from.
For this example we will use all the fields in our field type, however it could be limited to some predefined subset. We do this by
using the recurse_fields and string_to_fieldpath utilities:
1 local function construct_part_structure()
2 local part_structure = terralib.newlist()
3 local field_strings = {}
4 local type_table = {}
5 for k, v in pairs(part.fields) do
6 recursive_fields.recurse_field(v, field_strings, type_table)
7 end
8 for k, _ in pairs(field_strings) do
9 part_structure:insert({field = string_to_field_path.get_field_path(field_strings[k])})
10 end
11 return part_structure
12 end
13 local part_structure = construct_part_structure()
Line 2 creates our Terra list, whilst lines 3 and 4 construct the tables required for the recursive_fields utility. We then use the
recursive_field utility on our part field space in lines 5 to 7, giving us tables containing all of the (recusively defined) field names
and types.
In lines 8 to 10, we loop over the field names, and construct the corresponding field paths using the string_to_field_path utility. These are
then inserted into the Terra list as a table. In this case the table only contains the field path (accessed through field), but we could also choose
to store the type, or a default value, secondary field path etc. as our use case requires.
The Terra list is then returned in line 11, and in line 13 we create a local variable containing the list named part_structure.
Copying between regions.¶
To implement our copy functionality, we can use the previously constructure Terra list inside a Regent task. Lets imagine we have two identically
sized regions of particles, and want to copy all of the data from region1 to region2, we can create a task:
task copy_task(region1 : region(ispace(int1d), part), region2 : region(ispace(int1d), part)) where
reads(region1), writes(region2) do
--In this case we're assuming region1 and region2 are identically sized.
for part in [region1].ispace do
--Need to write our copy code here.
end
end
We can use the Terra list’s map function, which creates a map between every element of the list from A->B. In this case we want to go from our list (A)
to a quote expression containing the code to copy the values between the regions (B). This map is called inside an escape as we’re generating Regent code using Lua:
[part_structure:map( function(element)
return rquote
region2[part].[element.field] = region1[part].[element.field]
end
end)]
We define that the element variable is each element of the part_structure list (the table we input previously). We then create a quote, and access the
two regions at the part index, and splice the .field field path to access the same field from both regions.
Design of module imports.¶
The initial strategy for importing modules was to have everything separate, hence the complete separation of the 2D and 3D periodic tradequeue system. However, when designing the non-periodic version, it became apparent that we could create unique 2D and 3D headers, and have each other header required shared between the modules. This is done by the version-specific header setting a global value:
DSL_DIMENSIONALITY = 2
While this pollutes the global namespace, this can be a reserved variable name. With this set, the shared headers then do:
if DSL_DIMENSIONALITY == 2 then
neighbour_init = require("src/neighbour_search/cell_pair_tradequeues_nonperiod/2d_neighbour_init")
elseif DSL_DIMENSIONALITY == 3 then
neighbour_init = require("src/neighbour_search/cell_pair_tradequeues_nonperiod/3d_neighbour_init")
end
return neighbour_init
which enables importing only the necessary modules.
If possible, it would be nice to be able to import these headers with a function call, e.g.
require("path/to/nonperiodic_header")
set_dimensionality(2)
and have that setup the appropriate system, however we have not yet tried implementing this setup.