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