Source code for ddg.datastructures.nets.traverser

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

import numpy as np

# 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")