from contextlib import suppress import numpy as np from sympy import finite_diff_weights as fd_w with suppress(ImportError): import pytest from devito import ( Abs, Constant, Eq, Function, Grid, Inc, Operator, SubDimension, SubDomain, div, sin, warning ) from devito.builtins import gaussian_smooth, initialize_function, mmax, mmin from devito.tools import as_tuple __all__ = [ 'Model', 'ModelElastic', 'ModelViscoacoustic', 'ModelViscoelastic', 'SeismicModel', ] def initialize_damp(damp, padsizes, spacing, abc_type="damp", fs=False): """ Initialize damping field with an absorbing boundary layer. Parameters ---------- damp : Function The damping field for absorbing boundary condition. nbl : int Number of points in the damping layer. spacing : Grid spacing coefficient. mask : bool, optional whether the dampening is a mask or layer. mask => 1 inside the domain and decreases in the layer not mask => 0 inside the domain and increase in the layer """ eqs = [Eq(damp, 1.0 if abc_type == "mask" else 0.0)] for (nbl, nbr), d in zip(padsizes, damp.dimensions, strict=True): if not fs or d is not damp.dimensions[-1]: dampcoeff = 1.5 * np.log(1.0 / 0.001) / (nbl) # left dim_l = SubDimension.left(name=f'abc_{d.name}_l', parent=d, thickness=nbl) pos = Abs((nbl - (dim_l - d.symbolic_min) + 1) / float(nbl)) val = dampcoeff * (pos - sin(2*np.pi*pos)/(2*np.pi)) val = -val if abc_type == "mask" else val eqs += [Inc(damp.subs({d: dim_l}), val/d.spacing)] # right dampcoeff = 1.5 * np.log(1.0 / 0.001) / (nbr) dim_r = SubDimension.right(name=f'abc_{d.name}_r', parent=d, thickness=nbr) pos = Abs((nbr - (d.symbolic_max - dim_r) + 1) / float(nbr)) val = dampcoeff * (pos - sin(2*np.pi*pos)/(2*np.pi)) val = -val if abc_type == "mask" else val eqs += [Inc(damp.subs({d: dim_r}), val/d.spacing)] Operator(eqs, name='initdamp')() class PhysicalDomain(SubDomain): name = 'physdomain' def __init__(self, so, fs=False): super().__init__() self.so = so self.fs = fs def define(self, dimensions): map_d = {d: d for d in dimensions} if self.fs: map_d[dimensions[-1]] = ('middle', self.so, 0) return map_d class FSDomain(SubDomain): name = 'fsdomain' def __init__(self, so): super().__init__() self.size = so def define(self, dimensions): """ Definition of the upper section of the domain for wrapped indices FS. """ return {d: (d if d != dimensions[-1] else ('left', self.size)) for d in dimensions} class GenericModel: """ General model class with common properties """ def __init__(self, origin, spacing, shape, space_order, nbl=20, dtype=np.float32, subdomains=(), bcs="damp", grid=None, fs=False, topology=None): self.shape = shape self.space_order = space_order self.nbl = int(nbl) self.origin = tuple([dtype(o) for o in origin]) self.fs = fs # Default setup origin_pml = [dtype(o - s*nbl) for o, s in zip(origin, spacing, strict=True)] shape_pml = np.array(shape) + 2 * self.nbl # Model size depending on freesurface physdomain = PhysicalDomain(space_order, fs=fs) subdomains = subdomains + (physdomain,) if fs: fsdomain = FSDomain(space_order) subdomains = subdomains + (fsdomain,) origin_pml[-1] = origin[-1] shape_pml[-1] -= self.nbl # Origin of the computational domain with boundary to inject/interpolate # at the correct index if grid is None: # Physical extent is calculated per cell, so shape - 1 extent = tuple(np.array(spacing) * (shape_pml - 1)) self.grid = Grid(extent=extent, shape=shape_pml, origin=origin_pml, dtype=dtype, subdomains=subdomains, topology=topology) else: self.grid = grid self._physical_parameters = set() self.damp = None self._initialize_bcs(bcs=bcs) def _initialize_bcs(self, bcs="damp"): # Create dampening field as symbol `damp` if self.nbl == 0: self.damp = 1 if bcs == "mask" else 0 return # First initialization init = self.damp is None # Get current Function if already initialized self.damp = self.damp or Function(name="damp", grid=self.grid, space_order=self.space_order) if callable(bcs): bcs(self.damp, self.nbl) else: re_init = ((bcs == "mask" and mmin(self.damp) == 0) or (bcs == "damp" and mmax(self.damp) == 1)) if init or re_init: if re_init and not init: bcs_o = "damp" if bcs == "mask" else "mask" warning(f"Re-initializing damp profile from {bcs_o} to {bcs}") warning(f"Model has to be created with `bcs=\"{bcs}\"`" "for this WaveSolver") initialize_damp(self.damp, self.padsizes, self.spacing, abc_type=bcs, fs=self.fs) self._physical_parameters.update(['damp']) @property def padsizes(self): """ Padding size for each dimension. """ padsizes = [(self.nbl, self.nbl) for _ in range(self.dim-1)] padsizes.append((0 if self.fs else self.nbl, self.nbl)) return padsizes def physical_params(self, **kwargs): """ Return all set physical parameters and update to input values if provided """ known = [getattr(self, i) for i in self.physical_parameters] return {i.name: kwargs.get(i.name, i) or i for i in known} def _gen_phys_param(self, field, name, space_order, default_value=0, avg_mode='arithmetic', **kwargs): if field is None: return default_value if isinstance(field, np.ndarray): function = Function(name=name, grid=self.grid, space_order=space_order, avg_mode=avg_mode) initialize_function(function, field, self.padsizes) else: function = Constant(name=name, value=field, dtype=self.grid.dtype) self._physical_parameters.update([name]) return function @property def physical_parameters(self): return as_tuple(self._physical_parameters) @property def dim(self): """ Spatial dimension of the problem and model domain. """ return self.grid.dim @property def spacing(self): """ Grid spacing for all fields in the physical model. """ return self.grid.spacing @property def space_dimensions(self): """ Spatial dimensions of the grid """ return self.grid.dimensions @property def spacing_map(self): """ Map between spacing symbols and their values for each `SpaceDimension`. """ return self.grid.spacing_map @property def dtype(self): """ Data type for all associated data objects. """ return self.grid.dtype @property def domain_size(self): """ Physical size of the domain as determined by shape and spacing """ return tuple((d-1) * s for d, s in zip(self.shape, self.spacing, strict=True)) class SeismicModel(GenericModel): """ The physical model used in seismic inversion processes. Parameters ---------- origin : tuple of floats Origin of the model in m as a tuple in (x,y,z) order. spacing : tuple of floats Grid size in m as a Tuple in (x,y,z) order. shape : tuple of int Number of grid points size in (x,y,z) order. space_order : int Order of the spatial stencil discretisation. vp : array_like or float Velocity in km/s. nbl : int, optional The number of absorbin layers for boundary damping. bcs: str or callable Absorbing boundary type ("damp" or "mask") or initializer. dtype : np.float32 or np.float64 Defaults to np.float32. epsilon : array_like or float, optional Thomsen epsilon parameter (0