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')