Source code for ddg.datastructures.nets.traverser

import itertools as itts
import functools as fcts
import numpy as np
from collections.abc import Callable, Iterable

#TODO: Consider case when `idx` function is not defined by `Traverser`.

[docs]class Traverser: r"""Implement methods to traverse a discrete net. This is a stack of walkers for (parts of) a discrete net with :math:`\mathbb{Z}^n` combinatorics. Walkers are generator functions returning generators for indices of the net. """
[docs] @classmethod def Diag(cls, *args, shape='triang', dirct='NE', mode=1, bdy=True, shift=(0,0)): """ Setup a diagonal traversing scheme. Parameters ---------- args : float Number of vertex points in side(s) of `shape`. The number of required/accepted arguments depends on `shape`. shape : {'half', 'triang'}, optional Select the general shape the traverser operates on. dirct : {'NE', 'SW'}, optional Select the direction of movement for the traverser. mode : {1, ..., 8}, optional Select a special type of the choosen shape. The number of modes depends on the shape. bdy : bool, optional Decide whether to include the boundary of shape (if exists). Set `True` to include. shift : tuple of int, optional Constant shift for traversed shape. Returns ------- Traverser Notes ----- * The number of vertex points is calculated from each element of `args` by ceiling its absolut values. * Elements of `args` need to be finite. """ #TODO: More Notes on input!!! #TODO: Implement all cases, i.e. use `mode`!!! #TODO: Use `bdy` option. #TODO: Allow infinite values of `args`. try: dirct = dirct.upper() except AttributeError: raise TypeError('The `dirct` option must be a string, not '\ + dirct.__class__.__name__) if shape == 'half': tmp = [int(np.ceil(np.abs(val))) for val in args] if len(tmp) == 0: raise ValueError('At least one positional argument is needed\ when using the `half` option') elif len(tmp) == 1: n, m = tmp[0], tmp[0] elif len(tmp) == 2: n, m = tmp else: raise ValueError('At most two positional arguments are\ allowed when using the `quad` option.') if dirct == 'NE': def tmp(): for k in range(m): i, j = shift[0]+k, shift[1] for l in range(n): yield (i+l,j+l) def idx(i,j): i, j = i-shift[0], j-shift[1] return (i-j)*n + j elif dirct == 'SW': def tmp(): for k in range(m): i, j = shift[0]+k, shift[1] for l in range(n): yield (i-l,j-l) def idx(i,j): i, j = i-shift[0], j-shift[1] return (i-j)*n - j else: raise ValueError('The `dirct` option must be `NE` or `SW`, not: ' + str(dirct)) elif shape == 'triang': tmp = [int(np.ceil(np.abs(val))) for val in args] if len(tmp) == 0: raise ValueError('At least one positional argument is needed\ when using the `triang` option') elif len(tmp) == 1: n = tmp[0] else: raise ValueError('At most one positional argument is allowed\ when using the `triang` option') if dirct == 'NE': def tmp(): for k in range(n): i, j = shift[0]+k, shift[1] for l in range(n-k): yield (i+l,j+l) def idx(i,j): i, j = i-shift[0], j-shift[1] k = i - j return k*(2*n-k+1)//2 + j elif dirct == 'SW': def tmp(): for k in range(n): i, j = shift[0], shift[1]-k for l in range(n-k): yield (i-l,j-l) def idx(i,j): i, j = i-shift[0], j-shift[1] k = i - j return k*(2*n-k+1)//2 - i else: raise ValueError('The `dirct` option must be `NE` or `SW`, not: ' + str(dirct)) else: raise ValueError('The `shape` parameter must be `half`\ or `triang`, not '+shape.__class__.__name__) return cls(tmp, idx=idx)
[docs] @classmethod def Linear(cls, *args, intervals=None, shape='quad', dirct='N', mode=1, bdy=True, shift=(0,0)): """ Setup a linear traversing scheme. Parameters ---------- args : float Number of vertex points in side(s) of `shape`. The number of required/accepted arguments depends on `shape`. intervalls : list of int tuples, optional Side intervalls of a rectangular domain. shape : {'quad', 'triang'}, optional Select the general shape the traverser operates on. dirct : {'N', 'S', 'E', 'W'}, optional Select the direction of movement for the traverser. mode : {1, ..., 8}, optional Select a special type of the choosen shape. The number of modes depends on the shape. bdy : bool, optional Decide whether to include the boundary of shape (if exists). Set `True` to include. shift : tuple of int, optional Constant shift for traversed shape. Returns ------- Traverser Notes ----- * The number of vertex points is calculated from each element of `args` by ceiling its absolut values. * | If an element of `args` is `inf`, the half axis is taken as a side. | WARNING: | This might not produce a traverser finding all points in the | given `shape`. """ #TODO: More Notes on input!!! #TODO: Let `idx`-functions map to `inf` for non reachable index pairs. #TODO: Implement all cases, i.e. use `mode`!!! #TODO: Use `bdy` option. def setup_range(val, offset): if val != float('inf'): return range(offset, int(val)) else: return itts.count(offset) try: dirct = dirct.upper() except AttributeError: raise TypeError('The `dirct` option must be a string, not '\ + dirct.__class__.__name__) if intervals: if sum([b-a+1 for a,b in intervals]) == float('inf'): def tmp(): return iter(()) def idx(*args): return None else: def tmp(): return itts.product(*[range(a, b+1) for a,b in intervals]) widths = [b-a+1 for a,b in intervals] offsets = [a for a,b in intervals] mult = lambda x, y: x*y factors = [fcts.reduce(mult, widths[i:]) for i in range(1, len(widths))] + [1] def idx(*args): indices = [(i-o)%w for i,o,w in zip(args, offsets, widths)] return sum([i*factor for i,factor in zip(indices, factors)]) elif shape == 'quad': tmp = [np.ceil(np.abs(val)) for val in args] if len(tmp) == 0: raise ValueError('At least one positional argument is needed\ when using the `quad` option.') elif len(tmp) == 1: n, m = tmp[0], tmp[0] elif len(tmp) == 2: n, m = tmp else: raise ValueError('At most two positional arguments are\ allowed when using the `quad` option.') if dirct == 'N': n_range, m_range = setup_range(n, 0), setup_range(m, 0) def tmp(): for k in n_range: i, j = shift[0]+k, shift[1] for l in m_range: yield (i,j+l) if m == float('inf'): def idx(i,j): i, j = i-shift[0], j-shift[1] if i == 0: return j else: return float('inf') else: m = int(m) def idx(i,j): i, j = i-shift[0], j-shift[1] return i*m + j elif dirct == 'S': n_range, m_range = setup_range(n, 0), setup_range(m, 0) def tmp(): for k in n_range: i, j = shift[0]+k, shift[1] for l in m_range: yield (i,j-l) if m == float('inf'): def idx(i,j): i, j = i-shift[0], j-shift[1] if i == 0: return j else: return float('inf') else: def idx(i,j): i, j = i-shift[0], j-shift[1] return i*m - j elif dirct == 'E': n_range, m_range = setup_range(n, 0), setup_range(m, 0) def tmp(): for k in m_range: i, j = shift[0], shift[1]+k for l in n_range: yield (i+l,j) if n == float('inf'): def idx(i,j): i, j = i-shift[0], j-shift[1] if j == 0: return i else: return float('inf') else: def idx(i,j): i, j = i-shift[0], j-shift[1] return j*n + i elif dirct == 'W': n_range, m_range = setup_range(n, 0), setup_range(m, 0) def tmp(): for k in m_range: i, j = shift[0], shift[1]+k for l in n_range: yield (i-l,j) if n == float('inf'): def idx(i,j): i, j = i-shift[0], j-shift[1] if j == 0: return i else: return float('inf') else: def idx(i,j): i, j = i-shift[0], j-shift[1] return j*n - i else: raise ValueError('The `dirct` option must be `N`, `E`, `S`\ or `W`, not: {}'.format(dirct)) elif shape == 'triang': tmp = [np.ceil(np.abs(val)) for val in args] if len(tmp) == 0: raise ValueError('At least one positional argument is needed\ when using the `triang` option') elif len(tmp) == 1: n = tmp[0] if n == float('inf'): n_range = itts.count() inv_range = lambda k: itts.count() else: n = int(n) n_range = range(n) inv_range = lambda k: range(n-k) else: raise ValueError('At most one positional argument is allowed\ when using the `triang` option') if dirct == 'N': def tmp(): for k in n_range: i, j = shift[0]+k, shift[1] for l in range(k+1): yield (i,j+l) def idx(i,j): i, j = i-shift[0], j-shift[1] return i*(i+1)//2 + j elif dirct == 'S': def tmp(): for k in n_range: i, j = shift[0]+k, shift[1]+k for l in range(k+1): yield (i,j-l) def idx(i,j): i, j = i-shift[0], j-shift[1] return i*(i+1)//2 + (i-j) elif dirct == 'E': def tmp(): for k in n_range: i, j = shift[0]+k, shift[1]+k for l in inv_range(k): yield (i+l,j) def idx(i,j): i, j = i-shift[0], j-shift[1] return j*(2*n-j+1)//2 + (i-j) elif dirct == 'W': if n == float('inf'): raise ValueError('`dirct="W"` is not supported for\ `shape="trinag"`') def tmp(): for k in n_range: i, j = shift[0]+(n-1), shift[1]+k for l in inv_range(k): yield (i-l,j) def idx(i,j): i, j = i-shift[0], j-shift[1] return j*(2*n-j+1)//2 + n-1-i else: raise ValueError('The `dirct` option must be `N`, `E`, `S`\ or `W`, not: ' + str(dirct)) else: raise ValueError('The `shape` parameter must be `quad`\ or `triang`, not'+shape.__class__.__name__) return cls(tmp, idx=idx)
[docs] @classmethod def Circular(cls, shape='half', dirct='ACW', mode=1, bdy=True, shift=(0,0)): """ Setup a rectangular index domain. Parameters ---------- shape : {'quad', 'half'}, optional Select the general shape the traverser operates on. dirct : {'CW', 'ACW'}, optional Select the direction of movement for the traverser. mode : {1, ..., 6}, optional Select a special type of the choosen shape. The number of modes depends on the shape. bdy : bool, optional Decide whether to include the boundary of shape (if exists). Set `True` to include. shift : tuple of int, optional Constant shift for traversed shape. Returns ------- Traverser """ #TODO: More Notes on input!!! #TODO: Implement, all cases, i.e. use `mode`!!! #TODO: Use `bdy` option. try: dirct = dirct.upper() except AttributeError: raise TypeError('The `dirct` option must be a string, not '\ + dirct.__class__.__name__) if shape == 'half': if dirct == 'CW': def tmp(): for n in itts.count(): i, j = shift[0]+n, shift[1]+n for _ in range(2*n): yield (i,j) j -= 1 for _ in range(2*n): yield (i,j) i -= 1 yield (i,j) def idx(i,j): i, j = i-shift[0], j-shift[1] if i >= -j: return 2*(i**2) - j else: k = -j return 2*(k**2) - i + 2*k elif dirct == 'ACW': def tmp(): for n in itts.count(): i, j = shift[0]-n, shift[1]-n for _ in range(2*n): yield (i,j) i +=1 for _ in range(2*n): yield (i,j) j +=1 yield (i,j) def idx(i,j): i, j = i-shift[0], j-shift[1] if i <= -j: k = -j return 2*(k**2) + i else: return 2*(i**2) + j + 2*i else: raise ValueError('The `dirct` option must be `CW` or `ACW`, not: ' + str(dirct)) elif shape == 'quad': if dirct == 'CW': def tmp(): for n in itts.count(): i, j = shift[0], shift[1]+n for _ in range(n): yield (i,j) i += 1 for _ in range(n): yield (i,j) j -= 1 yield (i,j) def idx(i,j): i, j = i-shift[0], j-shift[1] if i <= j: return j**2 + i else: return i**2 + 2*i - j elif dirct == 'ACW': def tmp(): for n in itts.count(): i, j = shift[0]+n, shift[1] for _ in range(n): yield (i,j) j += 1 for _ in range(n): yield (i,j) i -=1 yield (i,j) def idx(i,j): i, j = i-shift[0], j-shift[1] if i >= j: return i**2 + j else: return j**2 + 2*j - i else: raise ValueError('The `dirct` option must be `CW` or `ACW`, not: ' + str(dirct)) else: raise ValueError('The `shape` parameter must be `half` or `quad`,\ not'+shape.__class__.__name__) return cls(tmp, idx=idx)
def __init__(self, gens, **kwargs): if isinstance(gens, Callable): self._walkers = [gens] elif isinstance(gens, Iterable): self._walkers = list(gens) else: raise TypeError('Traverser initialized by a generator\ function or an ordered iterable of\ those, not {}.'.format(gens.__class__.__name__)) self.idx = kwargs.get('idx', None) def __iter__(self): return itts.chain(*map(lambda fct: fct(), self._walkers))
[docs] def append(self, gen): self._walkers.append(gen)
[docs] def pop(self): try: return self._walkers.pop() except IndexError: raise IndexError('No walkers stored')