def interpolate(self, expr, offset=0, increment=False, self_subs={}): """ Generate equations interpolating an arbitrary expression into ``self``. Parameters ---------- expr : expr-like Input expression to interpolate. offset : int, optional Additional offset from the boundary. increment: bool, optional If True, generate increments (Inc) rather than assignments (Eq). """ variables = list(retrieve_function_carriers(expr)) # List of indirection indices for all adjacent grid points idx_subs, eqns = self._interpolation_indices(variables, offset) # Substitute coordinate base symbols into the interpolation coefficients args = [ expr.subs(v_sub) * b.subs(v_sub) for b, v_sub in zip(self._interpolation_coeffs, idx_subs) ] # Accumulate point-wise contributions into a temporary rhs = Scalar(name='sum', dtype=self.dtype) summands = [Eq(rhs, 0.)] + [Inc(rhs, i) for i in args] # Write/Incr `self` lhs = self.subs(self_subs) last = [Inc(lhs, rhs)] if increment else [Eq(lhs, rhs)] return eqns + summands + last
def _interpolation_indices(self, variables, offset=0): """ Generate interpolation indices for the DiscreteFunctions in ``variables``. """ index_matrix, points = self._index_matrix(offset) idx_subs = [] for i, idx in enumerate(index_matrix): # Introduce ConditionalDimension so that we don't go OOB mapper = {} for j, d in zip(idx, self.grid.dimensions): p = points[j] lb = sympy.And(p >= d.symbolic_min - self._radius, evaluate=False) ub = sympy.And(p <= d.symbolic_max + self._radius, evaluate=False) condition = sympy.And(lb, ub, evaluate=False) mapper[d] = ConditionalDimension(p.name, self._sparse_dim, condition=condition, indirect=True) # Track Indexed substitutions idx_subs.append(OrderedDict([(v, v.subs(mapper)) for v in variables if v.function is not self])) # Equations for the indirection dimensions eqns = [Eq(v, k) for k, v in points.items()] # Equations (temporaries) for the coefficients eqns.extend([Eq(p, c) for p, c in zip(self._point_symbols, self._coordinate_bases)]) return idx_subs, eqns
def inject(self, field, expr, offset=0): """ Generate equations injecting an arbitrary expression into a field. Parameters ---------- field : Function Input field into which the injection is performed. expr : expr-like Injected expression. offset : int, optional Additional offset from the boundary. """ expr = indexify(expr) field = indexify(field) p, _ = self.gridpoints.indices dim_subs = [] coeffs = [] for i, d in enumerate(self.grid.dimensions): rd = DefaultDimension(name="r%s" % d.name, default_value=self.r) dim_subs.append((d, INT(rd + self.gridpoints[p, i]))) coeffs.append(self.interpolation_coeffs[p, i, rd]) rhs = prod(coeffs) * expr field = field.subs(dim_subs) return [Eq(field, field + rhs.subs(dim_subs))]
def interpolate(self, expr, offset=0, increment=False, self_subs={}): """ Generate equations interpolating an arbitrary expression into ``self``. Parameters ---------- expr : expr-like Input expression to interpolate. offset : int, optional Additional offset from the boundary. increment: bool, optional If True, generate increments (Inc) rather than assignments (Eq). """ expr = indexify(expr) p, _, _ = self.interpolation_coeffs.indices dim_subs = [] coeffs = [] for i, d in enumerate(self.grid.dimensions): rd = DefaultDimension(name="r%s" % d.name, default_value=self.r) dim_subs.append((d, INT(rd + self.gridpoints[p, i]))) coeffs.append(self.interpolation_coeffs[p, i, rd]) # Apply optional time symbol substitutions to lhs of assignment lhs = self.subs(self_subs) rhs = prod(coeffs) * expr.subs(dim_subs) return [Eq(lhs, lhs + rhs)]
def _create_implicit_exprs(self): if not len(self._bounds) == 2 * len(self.dimensions): raise ValueError( "Left and right bounds must be supplied for each dimension") n_domains = self.n_domains i_dim = self.implicit_dimension dat = [] # Organise the data contained in 'bounds' into a form such that the # associated implicit equations can easily be created. for j in range(len(self._bounds)): index = floor(j / 2) d = self.dimensions[index] if j % 2 == 0: fname = d.min_name else: fname = d.max_name func = Function(name=fname, shape=(n_domains, ), dimensions=(i_dim, ), dtype=np.int32) # Check if shorthand notation has been provided: if isinstance(self._bounds[j], int): bounds = np.full((n_domains, ), self._bounds[j], dtype=np.int32) func.data[:] = bounds else: func.data[:] = self._bounds[j] dat.append(Eq(d.thickness[j % 2][0], func[i_dim])) return as_tuple(dat)
def _add_implicit(self, expressions): """ Create and add any associated implicit expressions. Implicit expressions are those not explicitly defined by the user but instead are requisites of some specified functionality. """ processed = [] seen = set() for e in expressions: if e.subdomain: try: dims = [d.root for d in e.free_symbols if isinstance(d, Dimension)] sub_dims = [d.root for d in e.subdomain.dimensions] dims = [d for d in dims if d not in frozenset(sub_dims)] dims.append(e.subdomain.implicit_dimension) if e.subdomain not in seen: processed.extend([i.func(*i.args, implicit_dims=dims) for i in e.subdomain._create_implicit_exprs()]) seen.add(e.subdomain) dims.extend(e.subdomain.dimensions) new_e = Eq(e.lhs, e.rhs, subdomain=e.subdomain, implicit_dims=dims) processed.append(new_e) except AttributeError: processed.append(e) else: processed.append(e) return processed
def interpolate(self, expr, offset=0, increment=False, self_subs={}): """ Generate equations interpolating an arbitrary expression into ``self``. Parameters ---------- expr : expr-like Input expression to interpolate. offset : int, optional Additional offset from the boundary. increment: bool, optional If True, generate increments (Inc) rather than assignments (Eq). """ # Derivatives must be evaluated before the introduction of indirect accesses try: expr = expr.evaluate except AttributeError: # E.g., a generic SymPy expression or a number pass variables = list(retrieve_function_carriers(expr)) # List of indirection indices for all adjacent grid points idx_subs, temps = self._interpolation_indices(variables, offset) # Substitute coordinate base symbols into the interpolation coefficients args = [ expr.xreplace(v_sub) * b.xreplace(v_sub) for b, v_sub in zip(self._interpolation_coeffs, idx_subs) ] # Accumulate point-wise contributions into a temporary rhs = Scalar(name='sum', dtype=self.dtype) summands = [Eq(rhs, 0., implicit_dims=self.dimensions)] summands.extend( [Inc(rhs, i, implicit_dims=self.dimensions) for i in args]) # Write/Incr `self` lhs = self.subs(self_subs) last = [Inc(lhs, rhs)] if increment else [Eq(lhs, rhs)] return temps + summands + last
def interpolate(self, expr, offset=0, u_t=None, p_t=None, cummulative=False): """Creates a :class:`sympy.Eq` equation for the interpolation of an expression onto this sparse point collection. :param expr: The expression to interpolate. :param offset: Additional offset from the boundary for absorbing boundary conditions. :param u_t: (Optional) time index to use for indexing into field data in `expr`. :param p_t: (Optional) time index to use for indexing into the sparse point data. :param cummulative: (Optional) If True, perform an increment rather than an assignment. Defaults to False. """ expr = indexify(expr) # Apply optional time symbol substitutions to expr if u_t is not None: time = self.grid.time_dim t = self.grid.stepping_dim expr = expr.subs(t, u_t).subs(time, u_t) variables = list(retrieve_indexed(expr)) # List of indirection indices for all adjacent grid points index_matrix = [ tuple(idx + ii + offset for ii, idx in zip(inc, self.coordinate_indices)) for inc in self.point_increments ] # Generate index substituions for all grid variables idx_subs = [] for i, idx in enumerate(index_matrix): v_subs = [(v, v.base[v.indices[:-self.grid.dim] + idx]) for v in variables] idx_subs += [OrderedDict(v_subs)] # Substitute coordinate base symbols into the coefficients subs = OrderedDict(zip(self.point_symbols, self.coordinate_bases)) rhs = sum([ expr.subs(vsub) * b.subs(subs) for b, vsub in zip(self.coefficients, idx_subs) ]) # Apply optional time symbol substitutions to lhs of assignment lhs = self if p_t is None else self.subs(self.indices[0], p_t) rhs = rhs + lhs if cummulative is True else rhs return [Eq(lhs, rhs)]
def __new__(cls, *args, **kwargs): if len(args) == 1: input_expr = args[0] assert isinstance(input_expr, Eq) obj = LoweredEq(input_expr) elif len(args) == 2: obj = LoweredEq(Eq(*args, evaluate=False)) else: raise ValueError("Cannot construct DummyEq from args=%s" % str(args)) return ClusterizedEq.__new__(cls, obj, ispace=obj.ispace, dspace=obj.dspace)
def guard(self, expr=None, offset=0): """ Generate guarded expressions, that is expressions that are evaluated by an Operator only if certain conditions are met. The introduced condition, here, is that all grid points in the support of a sparse value must fall within the grid domain (i.e., *not* on the halo). Parameters ---------- expr : expr-like, optional Input expression, from which the guarded expression is derived. If not specified, defaults to ``self``. offset : int, optional Relax the guard condition by introducing a tolerance offset. """ _, points = self._index_matrix(offset) # Guard through ConditionalDimension conditions = {} for d, idx in zip(self.grid.dimensions, self._coordinate_indices): p = points[idx] lb = sympy.And(p >= d.symbolic_min - offset, evaluate=False) ub = sympy.And(p <= d.symbolic_max + offset, evaluate=False) conditions[p] = sympy.And(lb, ub, evaluate=False) condition = sympy.And(*conditions.values(), evaluate=False) cd = ConditionalDimension("%s_g" % self._sparse_dim, self._sparse_dim, condition=condition) if expr is None: out = self.indexify().xreplace({self._sparse_dim: cd}) else: functions = { f for f in retrieve_function_carriers(expr) if f.is_SparseFunction } out = indexify(expr).xreplace( {f._sparse_dim: cd for f in functions}) # Temporaries for the indirection dimensions temps = [ Eq(v, k, implicit_dims=self.dimensions) for k, v in points.items() if v in conditions ] return out, temps
def _extract_increments(self, cluster, template, **kwargs): """ Extract the RHS of non-local tensor expressions performing an associative and commutative increment, and assign them to temporaries. """ processed = [] for e in cluster.exprs: if e.is_Increment and e.lhs.function.is_Input: handle = Scalar(name=template(), dtype=e.dtype).indexify() if q_scalar(e.rhs): extracted = e.rhs else: extracted = e.rhs.func(*[i for i in e.rhs.args if i != e.lhs]) processed.extend([Eq(handle, extracted), e.func(e.lhs, handle)]) else: processed.append(e) return cluster.rebuild(processed)
def _extract_nonaffine_indices(self, cluster, template, **kwargs): """ Extract non-affine array indices, and assign them to temporaries. """ make = lambda: Scalar(name=template(), dtype=np.int32).indexify() mapper = OrderedDict() for e in cluster.exprs: for indexed in retrieve_indexed(e): for i, d in zip(indexed.indices, indexed.function.indices): if q_affine(i, d) or q_scalar(i): continue elif i not in mapper: mapper[i] = make() processed = [Eq(v, k) for k, v in mapper.items()] processed.extend([e.xreplace(mapper) for e in cluster.exprs]) return cluster.rebuild(processed)
def _eliminate_inter_stencil_redundancies(self, cluster, template, **kwargs): """ Search aliasing expressions and capture them into vector temporaries. Examples -------- 1) temp = (a[x,y,z]+b[x,y,z])*c[t,x,y,z] >>> ti[x,y,z] = a[x,y,z] + b[x,y,z] temp = ti[x,y,z]*c[t,x,y,z] 2) temp1 = 2.0*a[x,y,z]*b[x,y,z] temp2 = 3.0*a[x,y,z+1]*b[x,y,z+1] >>> ti[x,y,z] = a[x,y,z]*b[x,y,z] temp1 = 2.0*ti[x,y,z] temp2 = 3.0*ti[x,y,z+1] """ # For more information about "aliases", refer to collect.__doc__ aliases = collect(cluster.exprs) # Redundancies will be stored in space-varying temporaries graph = FlowGraph(cluster.exprs) time_invariants = { v.rhs: graph.time_invariant(v) for v in graph.values() } # Find the candidate expressions processed = [] candidates = OrderedDict() for k, v in graph.items(): # Cost check (to keep the memory footprint under control) naliases = len(aliases.get(v.rhs)) cost = estimate_cost(v, True) * naliases test0 = lambda: cost >= self.MIN_COST_ALIAS and naliases > 1 test1 = lambda: cost >= self.MIN_COST_ALIAS_INV and time_invariants[ v.rhs] if test0() or test1(): candidates[v.rhs] = k else: processed.append(v) # Create alias Clusters and all necessary substitution rules # for the new temporaries alias_clusters = [] subs = {} for origin, alias in aliases.items(): if all(i not in candidates for i in alias.aliased): continue # The write-to Intervals writeto = [ Interval(i.dim, *alias.relaxed_diameter.get(i.dim, (0, 0))) for i in cluster.ispace.intervals if not i.dim.is_Time ] writeto = IntervalGroup(writeto) # Optimization: no need to retain a SpaceDimension if it does not # induce a flow/anti dependence (below, `i.offsets` captures this, by # telling how much halo will be needed to honour such dependences) dep_inducing = [i for i in writeto if any(i.offsets)] try: index = writeto.index(dep_inducing[0]) writeto = IntervalGroup(writeto[index:]) except IndexError: warning("Couldn't optimize some of the detected redundancies") # Create a temporary to store `alias` dimensions = [d.root for d in writeto.dimensions] halo = [(abs(i.lower), abs(i.upper)) for i in writeto] array = Array(name=template(), dimensions=dimensions, halo=halo, dtype=cluster.dtype) # Build up the expression evaluating `alias` access = tuple(i.dim - i.lower for i in writeto) expression = Eq(array[access], origin) # Create the substitution rules so that we can use the newly created # temporary in place of the aliasing expressions for aliased, distance in alias.with_distance: assert all(i.dim in distance.labels for i in writeto) access = [i.dim - i.lower + distance[i.dim] for i in writeto] if aliased in candidates: # It would *not* be in `candidates` if part of a composite alias subs[candidates[aliased]] = array[access] subs[aliased] = array[access] # Construct the `alias` IterationSpace intervals, sub_iterators, directions = cluster.ispace.args ispace = IterationSpace(intervals.add(writeto), sub_iterators, directions) # Construct the `alias` DataSpace mapper = detect_accesses(expression) parts = { k: IntervalGroup(build_intervals(v)).add(ispace.intervals) for k, v in mapper.items() if k } dspace = DataSpace(cluster.dspace.intervals, parts) # Create a new Cluster for `alias` alias_clusters.append(Cluster([expression], ispace, dspace)) # Switch temporaries in the expression trees processed = [e.xreplace(subs) for e in processed] return alias_clusters + [cluster.rebuild(processed)]
def _eliminate_inter_stencil_redundancies(self, cluster, template, **kwargs): """ Search for redundancies across the expressions and expose them to the later stages of the optimisation pipeline by introducing new temporaries of suitable rank. Two type of redundancies are sought: * Time-invariants, and * Across different space points Examples ======== Let ``t`` be the time dimension, ``x, y, z`` the space dimensions. Then: 1) temp = (a[x,y,z]+b[x,y,z])*c[t,x,y,z] >>> ti[x,y,z] = a[x,y,z] + b[x,y,z] temp = ti[x,y,z]*c[t,x,y,z] 2) temp1 = 2.0*a[x,y,z]*b[x,y,z] temp2 = 3.0*a[x,y,z+1]*b[x,y,z+1] >>> ti[x,y,z] = a[x,y,z]*b[x,y,z] temp1 = 2.0*ti[x,y,z] temp2 = 3.0*ti[x,y,z+1] """ if cluster.is_sparse: return cluster # For more information about "aliases", refer to collect.__doc__ mapper, aliases = collect(cluster.exprs) # Redundancies will be stored in space-varying temporaries g = cluster.trace indices = g.space_indices time_invariants = {v.rhs: g.time_invariant(v) for v in g.values()} # Find the candidate expressions processed = [] candidates = OrderedDict() for k, v in g.items(): # Cost check (to keep the memory footprint under control) naliases = len(mapper.get(v.rhs, [])) cost = estimate_cost(v, True)*naliases if cost >= self.MIN_COST_ALIAS and (naliases > 1 or time_invariants[v.rhs]): candidates[v.rhs] = k else: processed.append(v) # Create alias Clusters and all necessary substitution rules # for the new temporaries alias_clusters = ClusterGroup() rules = OrderedDict() for origin, alias in aliases.items(): if all(i not in candidates for i in alias.aliased): continue # Construct an iteration space suitable for /alias/ intervals, sub_iterators, directions = cluster.ispace.args intervals = [Interval(i.dim, *alias.relaxed_diameter.get(i.dim, i.limits)) for i in cluster.ispace.intervals] ispace = IterationSpace(intervals, sub_iterators, directions) # Optimization: perhaps we can lift the cluster outside the time dimension if all(time_invariants[i] for i in alias.aliased): ispace = ispace.project(lambda i: not i.is_Time) # Build a symbolic function for /alias/ intervals = ispace.intervals halo = [(abs(intervals[i].lower), abs(intervals[i].upper)) for i in indices] function = Array(name=template(), dimensions=indices, halo=halo) access = tuple(i - intervals[i].lower for i in indices) expression = Eq(function[access], origin) # Construct a data space suitable for /alias/ mapper = detect_accesses(expression) parts = {k: IntervalGroup(build_intervals(v)).add(intervals) for k, v in mapper.items() if k} dspace = DataSpace([i.zero() for i in intervals], parts) # Create a new Cluster for /alias/ alias_clusters.append(Cluster([expression], ispace, dspace)) # Add substitution rules for aliased, distance in alias.with_distance: access = [i - intervals[i].lower + j for i, j in distance if i in indices] rules[candidates[aliased]] = function[access] rules[aliased] = function[access] # Group clusters together if possible alias_clusters = groupby(alias_clusters).finalize() alias_clusters.sort(key=lambda i: i.is_dense) # Switch temporaries in the expression trees processed = [e.xreplace(rules) for e in processed] return alias_clusters + [cluster.rebuild(processed)]
def first_touch(array): """ Uses an Operator to initialize the given array in the same pattern that would later be used to access it. """ devito.Operator(Eq(array, 0.))()