def __init__(self, intervals, sub_iterators=None, directions=None): super(IterationSpace, self).__init__(intervals) self._sub_iterators = frozendict(sub_iterators or {}) if directions is None: self._directions = frozendict([(i.dim, Any) for i in self.intervals]) else: self._directions = frozendict(directions)
def __init__(self, exprs, ispace): # Derive halo exchanges self._mapper = {} scope = Scope(exprs) for f, (halos, local) in classify(scope).items(): if halos: loc_indices = compute_local_indices(f, local, ispace, scope) self._mapper[f] = HaloSchemeEntry(frozendict(loc_indices), frozenset(halos)) self._mapper = frozendict(self._mapper) # Track the IterationSpace offsets induced by SubDomains/SubDimensions. # These should be honored in the derivation of the `omapper` self._honored = {} # SubDimensions are not necessarily included directly in # ispace.dimensions and hence we need to first utilize the `_defines` method dims = set().union(*[ d._defines for d in ispace.dimensions if d._defines & self.dimensions ]) subdims = [d for d in dims if d.is_Sub and not d.local] for i in subdims: ltk, _ = i.thickness.left rtk, _ = i.thickness.right self._honored[i.root] = frozenset([(ltk, rtk)]) self._honored = frozendict(self._honored)
def __new__(cls, expr, *dims, **kwargs): if type(expr) == sympy.Derivative: raise ValueError( "Cannot nest sympy.Derivative with devito.Derivative") if not isinstance(expr, Differentiable): raise ValueError("`expr` must be a Differentiable object") new_dims, orders, fd_o, var_count = cls._process_kwargs( expr, *dims, **kwargs) # Construct the actual Derivative object obj = Differentiable.__new__(cls, expr, *var_count) obj._dims = tuple(OrderedDict.fromkeys(new_dims)) skip = kwargs.get('preprocessed', False) or obj.ndims == 1 obj._fd_order = fd_o if skip else DimensionTuple(*fd_o, getters=obj._dims) obj._deriv_order = orders if skip else DimensionTuple( *orders, getters=obj._dims) obj._side = kwargs.get("side") obj._transpose = kwargs.get("transpose", direct) obj._ppsubs = as_tuple(frozendict(i) for i in kwargs.get("subs", [])) obj._x0 = frozendict(kwargs.get('x0', {})) return obj
def __init__(self, exprs, ispace, dspace, guards=None, properties=None): self._exprs = tuple(ClusterizedEq(i, ispace=ispace, dspace=dspace) for i in as_tuple(exprs)) self._ispace = ispace self._dspace = dspace self._guards = frozendict(guards or {}) properties = dict(properties or {}) properties.update({i.dim: properties.get(i.dim, set()) for i in ispace.intervals}) self._properties = frozendict(properties)
def __init__(self, intervals, sub_iterators=None, directions=None): super(IterationSpace, self).__init__(intervals) # Normalize sub-iterators sub_iterators = sub_iterators or {} self._sub_iterators = frozendict([(k, tuple(filter_ordered(as_tuple(v)))) for k, v in sub_iterators.items()]) # Normalize directions if directions is None: self._directions = frozendict([(i.dim, Any) for i in self.intervals]) else: self._directions = frozendict(directions)
def _key(self, c): # Two Clusters/ClusterGroups are fusion candidates if their key is identical key = (frozenset(c.itintervals), c.guards) # We allow fusing Clusters/ClusterGroups even in presence of WaitLocks and # WithLocks, but not with any other SyncOps if isinstance(c, Cluster): sync_locks = (c.sync_locks, ) else: sync_locks = c.sync_locks for i in sync_locks: mapper = defaultdict(set) for k, v in i.items(): for s in v: if s.is_WaitLock or \ (self.fusetasks and s.is_WithLock): mapper[k].add(type(s)) else: mapper[k].add(s) mapper[k] = frozenset(mapper[k]) mapper = frozendict(mapper) key += (mapper, ) return key
def actions_from_unstructured(clusters, key, prefix, actions): it = prefix[-1] d = it.dim direction = it.direction # Locate the streamable Functions first_seen = {} last_seen = {} for c in clusters: candidates = key(c) if not candidates: continue for i in c.scope.accesses: f = i.function if f in candidates: k = (f, i[d]) first_seen.setdefault(k, c) last_seen[k] = c if not first_seen: return clusters callbacks = [(frozendict(first_seen), FetchPrefetch), (frozendict(last_seen), Delete)] # Create and map SyncOps to Clusters for seen, callback in callbacks: mapper = defaultdict(lambda: DefaultOrderedDict(list)) for (f, v), c in seen.items(): mapper[c][f].append(v) for c, m in mapper.items(): for f, v in m.items(): for fetch, s in indices_to_sections(v): if direction is Forward: ifetch = fetch.subs(d, d.symbolic_min) fcond = make_cond(c.guards.get(d), d, direction, d.symbolic_min) pfetch = fetch + 1 pcond = make_cond(c.guards.get(d), d, direction, d + 1) else: ifetch = fetch.subs(d, d.symbolic_max) fcond = make_cond(c.guards.get(d), d, direction, d.symbolic_max) pfetch = fetch - 1 pcond = make_cond(c.guards.get(d), d, direction, d - 1) syncop = callback(d, s, f, fetch, ifetch, fcond, pfetch, pcond) actions[c].syncs[d].append(syncop)
def __init__(self, exprs, ispace): # Derive the halo exchanges self._mapper = frozendict(classify(exprs, ispace)) # Track the IterationSpace offsets induced by SubDomains/SubDimensions. # These should be honored in the derivation of the `omapper` self._honored = {} # SubDimensions are not necessarily included directly in # ispace.dimensions and hence we need to first utilize the `_defines` method dims = set().union(*[d._defines for d in ispace.dimensions if d._defines & self.dimensions]) subdims = [d for d in dims if d.is_Sub and not d.local] for i in subdims: ltk, _ = i.thickness.left rtk, _ = i.thickness.right self._honored[i.root] = frozenset([(ltk, rtk)]) self._honored = frozendict(self._honored)
def _lookup_key(self, c, d): ispace = c.ispace.reset() dintervals = c.dspace.intervals.drop(d).reset() properties = frozendict( {d: relax_properties(v) for d, v in c.properties.items()}) return AliasKey(ispace, dintervals, c.dtype, None, properties)
def __init__(self, exprs, ispace=None, guards=None, properties=None, syncs=None): ispace = ispace or IterationSpace([]) self._exprs = tuple( ClusterizedEq(e, ispace=ispace) for e in as_tuple(exprs)) self._ispace = ispace self._guards = frozendict(guards or {}) self._syncs = frozendict(syncs or {}) properties = dict(properties or {}) properties.update( {i.dim: properties.get(i.dim, set()) for i in ispace.intervals}) self._properties = frozendict(properties)
def __init__(self, exprs, ispace): # Derive halo exchanges self._mapper = {} scope = Scope(exprs) for f, (halos, local) in classify(scope).items(): if halos: loc_indices = compute_local_indices(f, local, ispace, scope) self._mapper[f] = HaloSchemeEntry(frozendict(loc_indices), frozenset(halos)) self._mapper = frozendict(self._mapper) # Track the IterationSpace offsets induced by SubDomains/SubDimensions. # These should be honored in the derivation of the `omapper` self._honored = {} for i in ispace.intervals: if not i.dim._defines & set(self.dimensions): continue elif i.dim.is_Sub and not i.dim.local: ltk, _ = i.dim.thickness.left rtk, _ = i.dim.thickness.right self._honored[i.dim.root] = frozenset([(ltk, rtk)]) self._honored = frozendict(self._honored)
def _key(self, c): # Two Clusters/ClusterGroups are fusion candidates if their key is identical key = (frozenset(c.itintervals), c.guards) # We allow fusing Clusters/ClusterGroups with WaitLocks over different Locks, # while the WithLocks are to be kept separated (i.e. the remain separate tasks) if isinstance(c, Cluster): sync_locks = (c.sync_locks,) else: sync_locks = c.sync_locks for i in sync_locks: key += (frozendict({k: frozenset(type(i) if i.is_WaitLock else i for i in v) for k, v in i.items()}),) return key
def _cache_key(cls, *args, **kwargs): args = list(args) key = {} # The base type is necessary, otherwise two objects such as # `Scalar(name='s')` and `Dimension(name='s')` would have the same key key['cls'] = cls # The name is always present, and added as if it were an arg key['name'] = kwargs.pop('name', None) or args.pop(0) # From the args key['args'] = tuple(args) # From the kwargs key.update(kwargs) return frozendict(key)
def build(cls, fmapper, honored): obj = object.__new__(HaloScheme) obj._mapper = frozendict(fmapper) obj._honored = frozendict(honored) return obj
def conditionals(self): return self._conditionals or frozendict()
def classify(exprs, ispace): """ Produce the mapper ``Function -> HaloSchemeEntry``, which describes the necessary halo exchanges in the given Scope. """ scope = Scope(exprs) mapper = {} for f, r in scope.reads.items(): if not f.is_DiscreteFunction: continue elif f.grid is None: # TODO: improve me continue # For each data access, determine if (and what type of) a halo exchange # is required halo_labels = defaultdict(set) for i in r: v = {} for d in i.findices: if f.grid.is_distributed(d): if i.affine(d): thl, thr = i.touched_halo(d) # Note: if the left-HALO is touched (i.e., `thl = True`), then # the *right-HALO* is to be sent over in a halo exchange v[(d, LEFT)] = (thr and STENCIL) or IDENTITY v[(d, RIGHT)] = (thl and STENCIL) or IDENTITY else: v[(d, LEFT)] = STENCIL v[(d, RIGHT)] = STENCIL else: v[(d, i.aindices[d])] = NONE # Does `i` actually require a halo exchange? if not any(hl is STENCIL for hl in v.values()): continue # Derive diagonal halo exchanges from the previous analysis combs = list(product([LEFT, CENTER, RIGHT], repeat=len(f._dist_dimensions))) combs.remove((CENTER,)*len(f._dist_dimensions)) for c in combs: key = (f._dist_dimensions, c) if all(v.get((d, s)) is STENCIL or s is CENTER for d, s in zip(*key)): v[key] = STENCIL # Finally update the `halo_labels` for j, hl in v.items(): halo_labels[j].add(hl) if not halo_labels: continue # Distinguish between Dimensions requiring a halo exchange and those which don't up_loc_indices, halos = defaultdict(list), [] for (d, s), hl in halo_labels.items(): try: hl.remove(IDENTITY) except KeyError: pass if not hl: continue elif len(hl) > 1: raise HaloSchemeException("Inconsistency found while building a halo " "scheme for `%s` along Dimension `%s`" % (f, d)) elif hl.pop() is STENCIL: halos.append(Halo(d, s)) else: up_loc_indices[d].append(s) # Process the loc_indices. Consider: # 1) u[t+1, x] = f(u[t, x]) => shift == 1 # 2) u[t-1, x] = f(u[t, x]) => shift == 1 # 3) u[t+1, x] = f(u[t+1, x]) => shift == 0 # In the first and second cases, the x-halo should be inserted at `t`, # while in the last case it should be inserted at `t+1`. loc_indices = {} for d, aindices in up_loc_indices.items(): try: func = Max if ispace.is_forward(d.root) else Min except KeyError: # Max or Min is the same since `d` isn't an `ispace` Dimension func = Max candidates = [i for i in aindices if not is_integer(i)] candidates = {(i.origin if d.is_Stepping else i) - d: i for i in candidates} loc_indices[d] = candidates[func(*candidates.keys())] mapper[f] = HaloSchemeEntry(frozendict(loc_indices), frozenset(halos)) return mapper
def omapper(self): """ Logical decomposition of the DOMAIN region into OWNED and CORE sub-regions. This is "cumulative" over all DiscreteFunctions in the HaloScheme; it also takes into account IterationSpace offsets induced by SubDomains/SubDimensions. Examples -------- Consider a HaloScheme comprising two one-dimensional Functions, ``u`` and ``v``. ``u``'s halo, on the LEFT and RIGHT DataSides respectively, is (2, 2), while ``v``'s is (4, 4). The situation is depicted below. ^^oo----------------oo^^ u ^^^^oooo------------oooo^^^^ v Where '^' represents a HALO point, 'o' a OWNED point, and '-' a CORE point. Together, the 'o' and '-' points constitute the DOMAIN region. In this example, the "cumulative" OWNED size is (left=4, right=4), that is the max on each DataSide across all Functions, namely ``u`` and ``v``. The ``omapper`` will contain the following entries: [(((d, CORE, CENTER),), {d: (d_m + 4, d_M - 4)}), (((d, OWNED, LEFT),), {d: (d_m, min(d_m + 3, d_M))}), (((d, OWNED, RIGHT),), {d: (max(d_M - 3, d_m), d_M)})] In presence of SubDomains (or, more generally, iteration over SubDimensions), the "true" DOMAIN is actually smaller. For example, consider again the example above, but now with a SubDomain that excludes the first ``nl`` and the last ``nr`` DOMAIN points, where ``nl >= 0`` and ``nr >= 0``. Often, ``nl`` and ``nr`` are referred to as the "thickness" of the SubDimension (see also SubDimension.__doc__). For example, the situation could be as below ^^ooXXX----------XXXoo^^ u ^^^^ooooX----------Xoooo^^^^ v Where 'X' is a CORE point excluded by the computation due to the SubDomain. Here, the 'o' points are outside of the SubDomain, but in general they could also be inside. The ``omapper`` is constructed taking into account that SubDomains are iterated over with min point ``d_m + nl`` and max point ``d_M - nr``. Here, the ``omapper`` is: [(((d, CORE, CENTER),), {d: (d_m + 4, d_M - 4), nl: (max(nl - 4, 0),), nr: (max(nr - 4, 0),)}), (((d, OWNED, LEFT),), {d: (d_m, min(d_m + 3, d_M - nr)), nl: (nl,), nr: (0,)}), (((d, OWNED, RIGHT),), {d: (max(d_M - 3, d_m + nl), d_M), nl: (0,), nr: (nr,)})] To convince ourselves that this makes sense, we consider a number of cases. For now, we assume ``|d_M - d_m| > HALO``, that is the left-HALO and right-HALO regions do not overlap. 1. The SubDomain thickness is 0, which is like there were no SubDomains. By instantiating the template above with ``nl = 0`` and ``nr = 0``, it is trivial to see that we fall back to the non-SubDomain case. 2. The SubDomain thickness is as big as the HALO region size, that is ``nl = 4`` and ``nr = 4``. The ``omapper`` is such that no iterations will be performed in the OWNED regions (i.e., "everything is CORE"). 3. The SubDomain left-thickness is smaller than the left-HALO region size, while the SubDomain right-thickness is larger than the right-Halo region size. This means that some left-OWNED points are within the SubDomain, while the RIGHT-OWNED are outside. For example, take ``nl = 1`` and ``nr = 5``; the iteration regions will then be: - (CORE, CENTER): {d: (d_m + 4, d_M - 4), nl: (0,), nr: (1,)}, so the min point is ``d_m + 4``, while the max point is ``d_M - 5``. - (OWNED, LEFT): {d: (d_m, d_m + 3), nl: (1,), nr: (0,)}, so the min point is ``d_m + 1``, while the max point is ``dm + 3``. - (OWNED, RIGHT): {d: (d_M - 3, d_M), nl: (0,), nr: (5,)}, so the min point is ``d_M - 3``, while the max point is ``d_M - 5``, which implies zero iterations in this region. Let's now assume that the left-HALO and right-HALO regions overlap. For example, ``d_m = 0`` and ``d_M = 1`` (i.e., the DOMAIN only has two points), with the HALO size that is still (4, 4). 4. Let's take ``nl = 1`` and ``nr = 0``. That is, only one point is in the SubDomain and should be updated. We again instantiate the iteration regions and obtain: - (CORE, CENTER): {d: (d_m + 4, d_M - 4), nl: (0,), nr: (0,)}, so the min point is ``d_m + 4 = 4``, while the max point is ``d_M - 4 = -3``, which implies zero iterations in this region. - (OWNED, LEFT): {d: (d_m, min(d_m + 3, d_M - nr)), nl: (1,), nr: (0,)}, so the min point is ``d_m + 1 = 1``, while the max point is ``min(d_m + 3, d_M - nr) = min(3, 1) = 1``, which implies that there is exactly one point in this region. - (OWNED, RIGHT): {d: (max(d_M - 3, d_m + nl), d_M), nl: (0,), nr: (0,)}, so the min point is ``max(d_M - 3, d_m + nl) = max(-2, 1) = 1``, while the max point is ``d_M = 1``, which implies that there is exactly one point in this region, and this point is redundantly computed as it's logically the same as that in the (OWNED, LEFT) region. Notes ----- For each Function, the '^' and 'o' are exactly the same on *all MPI ranks*, so the output of this method is guaranteed to be consistent across *all MPI ranks*. """ items = [((d, CENTER), (d, LEFT), (d, RIGHT)) for d in self.dimensions] processed = [] for item in product(*items): where = [] mapper = {} for d, s in item: osl, osr = self.owned_size[d] # Handle SubDomain/SubDimensions to-honor offsets nl = Max(0, *[i for i, _ in self.honored.get(d, [])]) nr = Max(0, *[i for _, i in self.honored.get(d, [])]) if s is CENTER: where.append((d, CORE, s)) mapper[d] = (d.symbolic_min + osl, d.symbolic_max - osr) if nl != 0: mapper[nl] = (Max(nl - osl, 0),) if nr != 0: mapper[nr] = (Max(nr - osr, 0),) else: where.append((d, OWNED, s)) if s is LEFT: mapper[d] = (d.symbolic_min, Min(d.symbolic_min + osl - 1, d.symbolic_max - nr)) if nl != 0: mapper[nl] = (nl,) mapper[nr] = (0,) else: mapper[d] = (Max(d.symbolic_max - osr + 1, d.symbolic_min + nl), d.symbolic_max) if nr != 0: mapper[nl] = (0,) mapper[nr] = (nr,) processed.append((tuple(where), frozendict(mapper))) _, core = processed.pop(0) owned = processed return OMapper(core, owned)
def callback(self, clusters, prefix): if not prefix: return clusters it = prefix[-1] d = it.dim direction = it.direction try: pd = prefix[-2].dim except IndexError: pd = None # What are the stream-able Dimensions? # 0) all sequential Dimensions # 1) all CustomDimensions of fixed (i.e. integer) size, which # implies a bound on the amount of streamed data if all(SEQUENTIAL in c.properties[d] for c in clusters): make_fetch = lambda f, i, s, cb: FetchWaitPrefetch( f, d, direction, i, s, cb) make_delete = lambda f, i, s, cb: Delete(f, d, direction, i, s, cb) syncd = d elif d.is_Custom and is_integer(it.size): make_fetch = lambda f, i, s, cb: FetchWait(f, d, direction, i, it. size, cb) make_delete = lambda f, i, s, cb: Delete(f, d, direction, i, it. size, cb) syncd = pd else: return clusters first_seen = {} last_seen = {} for c in clusters: candidates = self.key(c) if not candidates: continue for i in c.scope.accesses: f = i.function if f in candidates: k = (f, i[d]) first_seen.setdefault(k, c) last_seen[k] = c if not first_seen: return clusters # Bind fetches and deletes to Clusters sync_ops = defaultdict(list) callbacks = [(frozendict(first_seen), make_fetch), (frozendict(last_seen), make_delete)] for seen, callback in callbacks: mapper = defaultdict(lambda: DefaultOrderedDict(list)) for (f, v), c in seen.items(): mapper[c][f].append(v) for c, m in mapper.items(): for f, v in m.items(): for i, s in indices_to_sections(v): next_cbk = make_next_cbk(c.guards.get(d), d, direction) sync_ops[c].append(callback(f, i, s, next_cbk)) # Attach SyncOps to Clusters processed = [] for c in clusters: v = sync_ops.get(c) if v is not None: processed.append( c.rebuild(syncs=normalize_syncs(c.syncs, {syncd: v}))) else: processed.append(c) return processed
def sync_locks(self): return frozendict({ k: tuple(i for i in v if i.is_SyncLock) for k, v in self.syncs.items() })
def __new__(cls, *args, **kwargs): if len(args) == 1 and isinstance(args[0], LoweredEq): # origin: LoweredEq(devito.LoweredEq, **kwargs) input_expr = args[0] expr = sympy.Eq.__new__(cls, *input_expr.args, evaluate=False) for i in cls._state: setattr(expr, '_%s' % i, kwargs.get(i) or getattr(input_expr, i)) return expr elif len(args) == 1 and isinstance(args[0], Eq): # origin: LoweredEq(devito.Eq) input_expr = expr = args[0] elif len(args) == 2: expr = sympy.Eq.__new__(cls, *args, evaluate=False) for i in cls._state: setattr(expr, '_%s' % i, kwargs.pop(i)) return expr else: raise ValueError("Cannot construct LoweredEq from args=%s " "and kwargs=%s" % (str(args), str(kwargs))) # Well-defined dimension ordering ordering = dimension_sort(expr) # Analyze the expression mapper = detect_accesses(expr) oobs = detect_oobs(mapper) conditional_dimensions = [i for i in ordering if i.is_Conditional] # Construct Intervals for IterationSpace and DataSpace intervals = build_intervals(Stencil.union(*mapper.values())) iintervals = [] # iteration Intervals dintervals = [] # data Intervals for i in intervals: d = i.dim if d in oobs: iintervals.append(i.zero()) dintervals.append(i) else: iintervals.append(i.zero()) dintervals.append(i.zero()) # Construct the IterationSpace iintervals = IntervalGroup(iintervals, relations=ordering.relations) iterators = build_iterators(mapper) ispace = IterationSpace(iintervals, iterators) # Construct the DataSpace dintervals.extend([ Interval(i, 0, 0) for i in ordering if i not in ispace.dimensions + conditional_dimensions ]) parts = { k: IntervalGroup(build_intervals(v)).add(iintervals) for k, v in mapper.items() if k } dspace = DataSpace(dintervals, parts) # Construct the conditionals conditionals = {} for d in conditional_dimensions: if d.condition is None: conditionals[d] = CondEq(d.parent % d.factor, 0) else: conditionals[d] = lower_exprs(d.condition) conditionals = frozendict(conditionals) # Lower all Differentiable operations into SymPy operations rhs = diff2sympy(expr.rhs) # Finally create the LoweredEq with all metadata attached expr = super(LoweredEq, cls).__new__(cls, expr.lhs, rhs, evaluate=False) expr._dspace = dspace expr._ispace = ispace expr._conditionals = conditionals expr._reads, expr._writes = detect_io(expr) expr._is_Increment = input_expr.is_Increment expr._implicit_dims = input_expr.implicit_dims return expr
def __new__(cls, *args, **kwargs): if len(args) == 1 and isinstance(args[0], LoweredEq): # origin: LoweredEq(devito.LoweredEq, **kwargs) input_expr = args[0] expr = sympy.Eq.__new__(cls, *input_expr.args, evaluate=False) for i in cls._state: setattr(expr, '_%s' % i, kwargs.get(i) or getattr(input_expr, i)) return expr elif len(args) == 1 and isinstance(args[0], Eq): # origin: LoweredEq(devito.Eq) input_expr = expr = args[0] elif len(args) == 2: expr = sympy.Eq.__new__(cls, *args, evaluate=False) for i in cls._state: setattr(expr, '_%s' % i, kwargs.pop(i)) return expr else: raise ValueError("Cannot construct LoweredEq from args=%s " "and kwargs=%s" % (str(args), str(kwargs))) # Well-defined dimension ordering ordering = dimension_sort(expr) # Analyze the expression accesses = detect_accesses(expr) dimensions = Stencil.union(*accesses.values()) # Separate out the SubIterators from the main iteration Dimensions, that # is those which define an actual iteration space iterators = {} for d in dimensions: if d.is_SubIterator: iterators.setdefault(d.root, set()).add(d) elif d.is_Conditional: # Use `parent`, and `root`, because a ConditionalDimension may # have a SubDimension as parent iterators.setdefault(d.parent, set()) else: iterators.setdefault(d, set()) # Construct the IterationSpace intervals = IntervalGroup([Interval(d, 0, 0) for d in iterators], relations=ordering.relations) ispace = IterationSpace(intervals, iterators) # Construct the conditionals and replace the ConditionalDimensions in `expr` conditionals = {} for d in ordering: if not d.is_Conditional: continue if d.condition is None: conditionals[d] = GuardFactor(d) else: conditionals[d] = diff2sympy(lower_exprs(d.condition)) if d.factor is not None: expr = uxreplace(expr, {d: IntDiv(d.index, d.factor)}) conditionals = frozendict(conditionals) # Lower all Differentiable operations into SymPy operations rhs = diff2sympy(expr.rhs) # Finally create the LoweredEq with all metadata attached expr = super(LoweredEq, cls).__new__(cls, expr.lhs, rhs, evaluate=False) expr._ispace = ispace expr._conditionals = conditionals expr._reads, expr._writes = detect_io(expr) expr._is_Increment = input_expr.is_Increment expr._implicit_dims = input_expr.implicit_dims return expr
def __init__(self, intervals, parts): super(DataSpace, self).__init__(intervals) self._parts = frozendict(parts)