11from abc import ABC
22from collections import namedtuple
33from functools import cached_property
4+ from itertools import product
45
56import numpy as np
67from sympy import prod
78
89from devito import configuration
9- from devito .data import LEFT , RIGHT
10+ from devito .data import LEFT , RIGHT , CENTER
1011from devito .logger import warning
1112from devito .mpi import Distributor , MPI , SubDistributor
12- from devito .tools import ReducerMap , as_tuple
13+ from devito .tools import ReducerMap , as_tuple , frozendict
1314from devito .types .args import ArgProvider
1415from devito .types .basic import Scalar
1516from devito .types .dense import Function
1920 MultiSubDimension , DefaultDimension )
2021from devito .deprecations import deprecations
2122
22- __all__ = ['Grid' , 'SubDomain' , 'SubDomainSet' ]
23+ __all__ = ['Grid' , 'SubDomain' , 'SubDomainSet' , 'Border' ]
2324
2425
2526GlobalLocal = 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