Skip to content

Commit db48c78

Browse files
Merge pull request #2646 from devitocodes/frame
dsl: Add an additional Border convenience class for boundary conditions
2 parents 56cf14c + d562262 commit db48c78

File tree

3 files changed

+832
-19
lines changed

3 files changed

+832
-19
lines changed

devito/types/grid.py

Lines changed: 286 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from abc import ABC
22
from collections import namedtuple
33
from functools import cached_property
4+
from itertools import product
45

56
import numpy as np
67
from sympy import prod
78

89
from devito import configuration
9-
from devito.data import LEFT, RIGHT
10+
from devito.data import LEFT, RIGHT, CENTER
1011
from devito.logger import warning
1112
from devito.mpi import Distributor, MPI, SubDistributor
12-
from devito.tools import ReducerMap, as_tuple
13+
from devito.tools import ReducerMap, as_tuple, frozendict
1314
from devito.types.args import ArgProvider
1415
from devito.types.basic import Scalar
1516
from devito.types.dense import Function
@@ -19,7 +20,7 @@
1920
MultiSubDimension, DefaultDimension)
2021
from devito.deprecations import deprecations
2122

22-
__all__ = ['Grid', 'SubDomain', 'SubDomainSet']
23+
__all__ = ['Grid', 'SubDomain', 'SubDomainSet', 'Border']
2324

2425

2526
GlobalLocal = namedtuple('GlobalLocal', 'glb loc')
@@ -871,7 +872,7 @@ def __subdomain_finalize_core__(self, grid):
871872
# Dimensions with identical names hash the same, hence tag them with the
872873
# SubDomainSet ID to make them unique so they can be used to key a dictionary
873874
# of replacements without risking overwriting.
874-
i_dim = Dimension('n_%s' % str(id(self)))
875+
i_dim = Dimension(f'n_{str(id(self))}')
875876
d_dim = DefaultDimension(name='d', default_value=2*grid.dim)
876877
sd_func = Function(name=self.name, grid=self._grid,
877878
shape=(self._n_domains, 2*grid.dim),
@@ -885,11 +886,12 @@ def __subdomain_finalize_core__(self, grid):
885886
sd_func.data[:, idx] = self._local_bounds[idx]
886887

887888
dimensions.append(MultiSubDimension(
888-
'i%s' % d.name, d, None, functions=sd_func,
889+
f'i{d.name}', d, None, functions=sd_func,
889890
bounds_indices=(2*i, 2*i+1), implicit_dimension=i_dim
890891
))
891892

892893
self._dimensions = tuple(dimensions)
894+
self._subfunction = sd_func
893895

894896
def __subdomain_finalize__(self):
895897
self.__subdomain_finalize_core__(self.grid)
@@ -907,6 +909,285 @@ def bounds(self):
907909
return self._local_bounds
908910

909911

912+
class Border(SubDomainSet):
913+
"""
914+
A convenience class for constructing a SubDomainSet which covers specified edges
915+
of the domain to a thickness of `border`.
916+
917+
By default, this border covers all sides of the grid. Alternatively, it is possible
918+
to add the border selectively to specific sides by supplying, for example,
919+
`dims={y: 'left'}` to obtain only a border on the left (from index zero) side of the
920+
y dimension, or `dims={x: x, y: 'left'}` to obtain borders on both sides of the x
921+
dimension, but only on the left of the y. One can also supply a single dimension on
922+
which to construct a border, using `dims=x` or similar.
923+
924+
Corners can be included, excluded, or overlapped by setting the `corners` kwarg.
925+
926+
Parameters
927+
----------
928+
grid : Grid
929+
The computational grid on which the border should be constructed
930+
border : int, tuple of int, or tuple of tuple of int
931+
The thickness of the border in gridpoints. A tuple with thickness for each
932+
dimension can be supplied if different thicknesses are required per-dimension.
933+
A tuple of tuples can also be supplied for more granular control of left and
934+
right border thicknesses for each dimension.
935+
dims : Dimension, dict, or None, optional
936+
The dimensions on which a border should be applied. Default is None, corresponding
937+
to borders on both sides of all dimensions.
938+
inset : int, tuple of int, or tuple of tuple of int, optional
939+
Inset the border from the edges by some number of gridpoints. Default is 0.
940+
name : str, optional
941+
A unique name for the SubDomainSet created. Default is 'border'.
942+
corners : str, optional
943+
Behaviour at the corners. Can be set to 'overlap' for overlapping subdomains at
944+
the corners, 'nooverlap' for non-overlapping corner subdomains, or 'nocorners'
945+
to omit the corners entirely. Default is `nooverlap`.
946+
947+
Examples
948+
--------
949+
Set up a border surrounding the whole grid:
950+
951+
>>> from devito import Grid, Border, Function, Eq, Operator
952+
>>> grid = Grid(shape=(7, 7))
953+
>>> x, y = grid.dimensions
954+
955+
>>> border = Border(grid, 2) # Border of thickness 2
956+
>>> f = Function(name='f', grid=grid, dtype=np.int32)
957+
>>> eq = Eq(f, f+1, subdomain=border)
958+
>>> summary = Operator(eq)()
959+
>>> f.data
960+
Data([[1, 1, 1, 1, 1, 1, 1],
961+
[1, 1, 1, 1, 1, 1, 1],
962+
[1, 1, 0, 0, 0, 1, 1],
963+
[1, 1, 0, 0, 0, 1, 1],
964+
[1, 1, 0, 0, 0, 1, 1],
965+
[1, 1, 1, 1, 1, 1, 1],
966+
[1, 1, 1, 1, 1, 1, 1]], dtype=int32)
967+
968+
Set up a border consisting of the right side of the x dimension and both sides
969+
of the y dimension:
970+
971+
>>> border2 = Border(grid, 2, dims={x: 'right', y: y})
972+
>>> g = Function(name='g', grid=grid, dtype=np.int32)
973+
>>> eq2 = Eq(g, g+1, subdomain=border2)
974+
>>> summary = Operator(eq2)()
975+
>>> g.data
976+
Data([[1, 1, 0, 0, 0, 1, 1],
977+
[1, 1, 0, 0, 0, 1, 1],
978+
[1, 1, 0, 0, 0, 1, 1],
979+
[1, 1, 0, 0, 0, 1, 1],
980+
[1, 1, 0, 0, 0, 1, 1],
981+
[1, 1, 1, 1, 1, 1, 1],
982+
[1, 1, 1, 1, 1, 1, 1]], dtype=int32)
983+
984+
Set up a border consisting of only the sides in the y dimension:
985+
986+
>>> border3 = Border(grid, 2, dims=y)
987+
>>> h = Function(name='h', grid=grid, dtype=np.int32)
988+
>>> eq3 = Eq(h, h+1, subdomain=border3)
989+
>>> summary = Operator(eq3)()
990+
>>> h.data
991+
Data([[1, 1, 0, 0, 0, 1, 1],
992+
[1, 1, 0, 0, 0, 1, 1],
993+
[1, 1, 0, 0, 0, 1, 1],
994+
[1, 1, 0, 0, 0, 1, 1],
995+
[1, 1, 0, 0, 0, 1, 1],
996+
[1, 1, 0, 0, 0, 1, 1],
997+
[1, 1, 0, 0, 0, 1, 1]], dtype=int32)
998+
999+
which is equivalent to:
1000+
1001+
>>> border4 = Border(grid, 2, dims={y: y})
1002+
>>> i = Function(name='i', grid=grid, dtype=np.int32)
1003+
>>> eq4 = Eq(i, i+1, subdomain=border4)
1004+
>>> summary = Operator(eq4)()
1005+
>>> i.data
1006+
Data([[1, 1, 0, 0, 0, 1, 1],
1007+
[1, 1, 0, 0, 0, 1, 1],
1008+
[1, 1, 0, 0, 0, 1, 1],
1009+
[1, 1, 0, 0, 0, 1, 1],
1010+
[1, 1, 0, 0, 0, 1, 1],
1011+
[1, 1, 0, 0, 0, 1, 1],
1012+
[1, 1, 0, 0, 0, 1, 1]], dtype=int32)
1013+
1014+
"""
1015+
1016+
DimSpec = None | dict[Dimension, Dimension | str]
1017+
ParsedDimSpec = frozendict[Dimension, Dimension | str]
1018+
1019+
BorderInt = int | np.integer
1020+
BorderSpec = BorderInt | tuple[BorderInt] | tuple[tuple[BorderInt]]
1021+
ParsedBorderSpec = tuple[tuple[BorderInt]]
1022+
1023+
def __init__(self, grid: Grid, border: BorderSpec,
1024+
dims: DimSpec = None, name: str = 'border',
1025+
inset: BorderSpec = 0, corners: str = 'nooverlap') -> None:
1026+
1027+
self._name = name
1028+
self._border = Border._parse_border(border, grid)
1029+
self._border_dims = Border._parse_dims(dims, grid)
1030+
self._inset = Border._parse_border(inset, grid, mode='inset')
1031+
1032+
if corners not in ('overlap', 'nooverlap', 'nocorners'):
1033+
raise ValueError(f"Unrecognised corners option: {corners}")
1034+
self._corners = corners
1035+
1036+
ndomains, bounds = self._build_domains(grid)
1037+
super().__init__(N=ndomains, bounds=bounds, grid=grid)
1038+
1039+
@property
1040+
def border(self):
1041+
return self._border
1042+
1043+
@property
1044+
def border_dims(self):
1045+
return self._border_dims
1046+
1047+
@property
1048+
def name(self):
1049+
return self._name
1050+
1051+
@property
1052+
def corners(self):
1053+
return self._corners
1054+
1055+
@property
1056+
def inset(self):
1057+
return self._inset
1058+
1059+
def _inset_flat(self, i):
1060+
"""Flattened access into self.inset"""
1061+
return self.inset[i // len(self.inset)][i % 2]
1062+
1063+
@staticmethod
1064+
def _parse_dims(dims: DimSpec, grid: Grid) -> ParsedDimSpec:
1065+
if dims is None:
1066+
_border_dims = {d: d for d in grid.dimensions}
1067+
elif isinstance(dims, Dimension):
1068+
_border_dims = {dims: dims}
1069+
elif isinstance(dims, dict):
1070+
_border_dims = dims
1071+
else:
1072+
raise ValueError("Dimensions should be supplied as a single dimension, or "
1073+
"a dict of the form `{x: x, y: 'left'}`")
1074+
1075+
return frozendict(_border_dims)
1076+
1077+
@staticmethod
1078+
def _parse_border(border: BorderSpec, grid: Grid,
1079+
mode: str = 'border') -> ParsedBorderSpec:
1080+
if isinstance(border, (int, np.integer)):
1081+
return ((border, border),)*len(grid.dimensions)
1082+
1083+
else: # Tuple guaranteed by typing
1084+
if not len(border) == len(grid.dimensions):
1085+
raise ValueError(f"Length of {mode} specification should "
1086+
"match number of dimensions")
1087+
retval = []
1088+
for b, d in zip(border, grid.dimensions):
1089+
if isinstance(b, tuple):
1090+
if not len(b) == 2:
1091+
raise ValueError(f"{b}: more than two thicknesses supplied "
1092+
f"for dimension {d}")
1093+
retval.append(b)
1094+
else:
1095+
retval.append((b, b))
1096+
1097+
return tuple(retval)
1098+
1099+
def _build_domains(self, grid: Grid) -> tuple[int, tuple[np.ndarray]]:
1100+
"""
1101+
Constructs the bounds and ndomains kwargs for the SubDomainSet.
1102+
"""
1103+
if self.corners == 'overlap':
1104+
return self._build_domains_overlap(grid)
1105+
else:
1106+
return self._build_domains_nooverlap(grid)
1107+
1108+
def _build_domains_overlap(self, grid: Grid) -> tuple[int, tuple[np.ndarray]]:
1109+
1110+
bounds = []
1111+
iterations = zip(grid.dimensions, grid.shape, self.border, strict=True)
1112+
for i, (d, s, b) in enumerate(iterations):
1113+
1114+
if d in self.border_dims:
1115+
# Note: slightly counterintuitive since a left-side boundary only has
1116+
# right-side thickness
1117+
bounds_l = [s - b[0] - self.inset[i][0] if j == 2*i+1
1118+
else self._inset_flat(j)
1119+
for j in range(2*len(grid.dimensions))]
1120+
bounds_r = [s - b[1] - self.inset[i][1] if j == 2*i
1121+
else self._inset_flat(j)
1122+
for j in range(2*len(grid.dimensions))]
1123+
1124+
side = self.border_dims[d]
1125+
if isinstance(side, Dimension):
1126+
bounds.extend([bounds_l, bounds_r])
1127+
elif side == 'left':
1128+
bounds.append(bounds_l)
1129+
elif side == 'right':
1130+
bounds.append(bounds_r)
1131+
else:
1132+
raise ValueError(f"Unrecognised side value: {side}")
1133+
1134+
# Need to transpose array to fit into expected format for SubDomainSet
1135+
return len(bounds), tuple(np.array(bounds).T)
1136+
1137+
def _build_domains_nooverlap(self, grid: Grid) -> tuple[int, tuple[np.ndarray]]:
1138+
domain_map = {} # Stores the side
1139+
interval_map = {} # Stores the mapping from the side to bounds
1140+
1141+
# Unpack the user-provided specification into a set of sides (on which
1142+
# a cartesian product is taken) and a mapper from those sides to a set of
1143+
# bounds for each dimension.
1144+
for d, s, b, i in zip(grid.dimensions, grid.shape, self.border, self.inset):
1145+
if d in self.border_dims:
1146+
side = self.border_dims[d]
1147+
1148+
if isinstance(side, Dimension):
1149+
domain_map[d] = (LEFT, CENTER, RIGHT)
1150+
interval_map[d] = {LEFT: (i[0], s - b[0] - i[0]),
1151+
CENTER: (b[0] + i[0], b[1] + i[1]),
1152+
RIGHT: (s - b[1] - i[1], i[1])}
1153+
elif side == 'left':
1154+
domain_map[d] = (LEFT, CENTER)
1155+
# For intuitive behaviour, 'nocorners' should always skip corners
1156+
centerval = b[1] + i[1] if self.corners == 'nocorners' else i[1]
1157+
interval_map[d] = {LEFT: (i[0], s - b[0] - i[0]),
1158+
CENTER: (b[0] + i[0], centerval)}
1159+
elif side == 'right':
1160+
domain_map[d] = (CENTER, RIGHT)
1161+
centerval = b[0] + i[0] if self.corners == 'nocorners' else i[0]
1162+
interval_map[d] = {CENTER: (centerval, b[1] + i[1]),
1163+
RIGHT: (s - b[1] - i[1], i[1])}
1164+
else:
1165+
raise ValueError(f"Unrecognised side value {side}")
1166+
else:
1167+
domain_map[d] = (CENTER,)
1168+
interval_map[d] = {CENTER: (0, 0)}
1169+
1170+
# Get the cartesian product, then select the required domains. The sides are used
1171+
# to make this step more straightforward.
1172+
maybe_domains = list(product(*domain_map.values()))
1173+
domains = []
1174+
for d in maybe_domains:
1175+
if not all(i is CENTER for i in d):
1176+
# Don't add any domains that are completely centered
1177+
if self.corners != 'nocorners' or any(i is CENTER for i in d):
1178+
# Don't add corners if 'no corners' option selected
1179+
domains.append([interval_map[dim][dom] for (dim, dom)
1180+
in zip(grid.dimensions, d)])
1181+
1182+
domains = np.array(domains)
1183+
1184+
# Reshape the bounds into the format expected by the SubDomainSet init
1185+
shape = (domains.shape[0], domains.shape[1]*domains.shape[2])
1186+
bounds = np.reshape(domains, shape).T
1187+
1188+
return domains.shape[0], tuple(bounds)
1189+
1190+
9101191
# Preset SubDomains
9111192

9121193

0 commit comments

Comments
 (0)