import itertools as itts
from collections.abc import Callable, Container, Iterable, Sized
import numpy as np
from ddg.nets._domain import (
DiscreteDomain,
DiscreteInterval,
Domain,
EmptyDomain,
SmoothDomain,
SmoothInterval,
SmoothRectangularDomain,
)
[docs]class Net(Callable):
"""
Base class for nets.
Parameters
----------
fct : function
Function defining the net.
domain : ddg.nets.Domain or list of intervals
Domain of `fct`.
name : str, optional
Name of the net (mostly used for blender conversion).
Attributes
----------
fct : function
Function defining the net.
domain : ddg.nets.Domain
Domain of `fct`.
name : str
Name of the net (mostly used for blender conversion).
dimension : int
Dimension of the domain.
See Also
--------
SmoothNet, DiscreteNet, NetCollection
"""
def __init__(self, fct, domain, *args, name="Net"):
self._fct = fct
if isinstance(domain, Domain):
self.domain = domain
else:
raise ValueError
# self.domain = domain
self.name = name
super().__init__()
@property
def fct(self):
return lambda *a: self._fct(*a)
@property
def dimension(self):
return self.domain.dimension
def __call__(self, *args, **kwargs):
raise NotImplementedError
[docs]class EmptyNet(Net, Iterable):
def __init__(self, coordinates, name="Empty"):
_coordinates = np.array(coordinates)
super().__init__(lambda: _coordinates, EmptyDomain(), name=name)
def __iter__(self):
return iter(())
def __call__(self):
return self.fct()
[docs]class PointNet(Net, Iterable):
def __init__(self, value, name="Point"):
if isinstance(value, Iterable):
tmp = np.array(value)
value = lambda: tmp
super().__init__(value, SmoothRectangularDomain([]), name=name)
def __iter__(self):
return iter((self(),))
def __call__(self):
return self.fct()
[docs]class SmoothNet(Net):
"""
Store and manage data of a smooth net.
Parameters
----------
fct : function
Function defining the net.
domain : ddg.nets.SmoothDomain or list of intervals
Domain of `fct`.
periodicity : set, optional
Set containing indices of periodic coordinate directions.
periodic : bool
Alternative way of specifying periodicity for one dimensional net.
It precedes periodicity if set.
name : str, optional
Name of the net (mostly used for blender conversion).
See Also
--------
Net, NetCollection
"""
def __new__(cls, fct, domain, *args, **kwargs):
# TODO: Better handling of arguments for object()
if isinstance(domain, SmoothDomain):
tmp = domain.dimension == 1
elif isinstance(domain, Iterable):
tmp = len(domain) == 1
else:
raise TypeError(
"Domain does not support given type: {}".format(
domain.__class__.__name__
)
)
if tmp:
return super(SmoothNet, cls).__new__(SmoothCurve)
else:
return super(SmoothNet, cls).__new__(cls)
def __init__(self, fct, domain, *args, name="SmoothNet"):
if not isinstance(domain, Domain):
domain = SmoothDomain(domain)
super().__init__(fct, domain, *args, name=name)
def __call__(self, *args):
return self.fct(*args)
[docs]class SmoothCurve(SmoothNet):
def __new__(cls, *args, **kwargs):
return super(SmoothNet, cls).__new__(SmoothCurve)
def __init__(self, fct, domain, *args, name="SmoothCurve"):
if not isinstance(domain, Domain):
domain = SmoothInterval(domain)
super().__init__(
fct,
domain,
*args,
name=name,
)
[docs]class DiscreteNet(Net, Iterable):
"""
Store and manage data of a discrete net.
Parameters
----------
fct : function
Function defining the net.
domain : ddg.nets.DiscreteDomain or list of intervals
Domain of 'fct'.
periodicity : set, optional
Set containing indices of periodic coordinate directions.
traverser : ddg.nets.traverser.Traverser, optional
Traversing scheme of the dicrete net.
name : str, optional
Name of the net (mostly used for blender conversion).
init : iterable or ddg.nets.DiscreteDomain, optional
Preset initial data or generate all data in the given domain.
hint : function, optional
Function returning the net points needed to compute the value
for a given net point (by default there is no hint function).
If specified the values are calculated iteratively.
Attributes
----------
traverser : ddg.nets.traverser.Traverser
The traverser of the Domain.
See Also
--------
Net, NetCollection
Notes
-----
* The `hint` function has to return an empty list for all net points
which are initialized by `fct`.
* The function `fct` will be called repeatedly to calculate the value
for the given index. It has to guarantee that the calculation will
stop, i.e. that it will hit initial data.::
E.g.: Suppose the missing vertex has the index (i,j). Then `fct`
could use the values at vertices ((i-1,j-1), (i,j-1), (i-1, j))
to calculate the missing value
(i-1,j) (i,j)
input -> o--------o <- missing
| |
| |
| |
input -> o--------o <- input
(i-1,j-1) (i,j-1)
If the standard axis {(i,0) | i int} and {(0,i) | i int}
are initial data (either given by `fct` itself or
previously set) this definition is well defined.
* If `hint` is not given the calculations are made recursively. So it
may easyly generate `RecursionError` for exeeding recursition depth.
Thus it is important to take the search pattern of `fct` into account
for choosing the traverser.::
E.g.: Suppose the inital data is given in a zick-zack pattern in the
fourth quadrant like
(0,0) o--o
|
(-1,-1) o--o
|
(-2,-2) o--o
If we suppose `fct` to use the search pattern from the comment
above a (finite) linear traverser will generate `RecursitionError`
for a relatively small search box (about 80x80). However a
diagonal traverser will guarantee a small recursition depth.
1-->2-->3-->4 1 5 8 10
\\ \\ \\
5-->6-->7 2 6 9
\\ \\
8-->9 3 7
\\
10 4
(lin. trav.) (diag. trav.)
"""
def __new__(cls, fct, domain, hint=None, **kwargs):
if isinstance(domain, DiscreteDomain):
tmp = domain.dimension == 1
elif isinstance(domain, Iterable):
tmp = DiscreteDomain(domain).dimension == 1
elif isinstance(domain, EmptyDomain):
tmp = len(np.shape(domain)) == 1
else:
raise TypeError(
"Domain does not support given type: {}".format(
domain.__class__.__name__
)
)
if tmp:
return super(DiscreteNet, cls).__new__(DiscreteCurve)
elif hint is not None:
return super(DiscreteNet, cls).__new__(DiscreteIterativeNet)
else:
return super(DiscreteNet, cls).__new__(DiscreteRecursiveNet)
def __init__(self, fct, domain, name="DiscreteNet", traverser=None, init=None):
if not isinstance(domain, Domain):
domain = DiscreteDomain(domain)
super().__init__(fct, domain, name=name)
self._init = init
self._data = {}
@property
def traverser(self):
"""
Get the traverser of the Domain.
Returns
-------
ddg.nets.traverser.Traverser
"""
return self.domain.traverser
def __iter__(self):
"""
Iterate over the discrete net using the set traverser.
Returns
-------
Generator
"""
for idx in self.traverser:
yield self[idx]
def _get(self, idx):
"""
Retrive or generate value of the net at `idx`.
Parameters
----------
idx : tuple
Index of the net point.
Returns
-------
object
Generated object at the given net point.
"""
raise NotImplementedError
def __getitem__(self, idx):
try:
if isinstance(idx, DiscreteDomain):
return (self[i] for i in idx.traverser)
if len(idx) == self.dimension:
return self._get(idx)
elif len(idx) > self.dimension:
msg = "Too many indices for {} dimensional DiscreteNet."
msg = msg.format(self.dimension)
raise IndexError(msg)
elif len(idx) == 0:
raise IndexError("At least one index or slice needed.")
else:
return self[
tuple(idx)
+ tuple(slice(None) for _ in range(self.dimension - len(idx)))
]
except TypeError:
if isinstance(idx, int) or isinstance(idx, slice):
return self[
(idx,) + tuple(slice(None) for _ in range(self.dimension - 1))
]
else:
error_message = " ".join(
[
"DiscreteNet indices must be DiscreteDomains,",
"iterable, integers or slices, not {}.",
]
).format(idx.__class__.__name__)
raise TypeError(error_message)
def __call__(self, *args):
return self.fct(*args)
[docs] def evaluate(self):
return list(self[self.domain])
[docs]class DiscreteRecursiveNet(DiscreteNet):
"""Recursive implementation of DiscreteNet."""
def __new__(cls, *args, **kwargs):
return super(DiscreteNet, cls).__new__(cls)
def _get(self, idx):
"""
Retrive or generate value of the net at `idx`.
Parameters
----------
idx : tuple
Index of the net point.
Returns
-------
object
Generated object at the given net point.
"""
gens, all_int = [], True
for el in idx:
try:
if isinstance(el, int):
gens.append((el,))
elif el.stop:
gens.append(range(*el.indices(el.stop)))
all_int = False
else:
gens.append(itts.count(el.start, el.step))
all_int = False
except AttributeError:
raise TypeError(
"Elements of indices must be integers or "
" slices, not {}.".format(el.__class__.__name__)
)
if all_int:
try:
return self._data[idx]
except KeyError:
self._data[idx] = self.fct(*idx)
return self._data[idx]
else:
return (self[i] for i in itts.product(*gens))
[docs]class DiscreteIterativeNet(DiscreteNet):
"""Iterative implementation of DiscreteNet."""
def __new__(cls, *args, **kwargs):
return super(DiscreteNet, cls).__new__(cls)
def __init__(self, *args, hint=None, **kwargs):
self._hint = hint
super().__init__(*args, **kwargs)
def _get(self, idx):
"""
Retrive or generate value of the net at `idx`.
Parameters
----------
idx : tuple
Index of the net point.
Returns
-------
object
Generated object at the given net point.
"""
gens, all_int = [], True
for el in idx:
try:
if isinstance(el, int):
gens.append((el,))
elif el.stop:
gens.append(range(*el.indices(el.stop)))
all_int = False
else:
gens.append(itts.count(el.start, el.step))
all_int = False
except AttributeError:
raise TypeError(
"Elements of indices must be integers or "
" slices, not {}.".format(el.__class__.__name__)
)
if all_int:
try:
return self.transformation(self._data[idx])
except KeyError:
stack = [[idx, False]]
while stack:
if stack[-1][0] not in self._data:
if stack[-1][1]:
self._data[stack[-1][0]] = self.fct(*stack[-1][0])
else:
stack[-1][1] = True
stack.extend(
[[el, False] for el in self._hint(stack[-1][0])]
)
else:
stack.pop()
return self.transformation(self._data[idx])
else:
return (self[i] for i in itts.product(*gens))
[docs]class DiscreteCurve(DiscreteRecursiveNet):
def __init__(self, fct, domain, *args, name="DiscreteCurve"):
if not isinstance(domain, Domain):
domain = DiscreteInterval(domain)
super().__init__(fct, domain, *args, name=name)
[docs]class NetCollection(Sized, Iterable, Container):
"""Collection of Nets
Parameters
----------
nets : List of nets
Nets to be contained in the collection.
name : str (default='NetCollection')
Name of the collection.
Attributes
----------
name : string
Name of the collection
dimension : int
Dimension of the (first) net inside the collection
"""
def __init__(self, nets=[], name="NetCollection"):
self.nets = nets
self.name = name
self._added_nets = []
super().__init__()
def __contains__(self, net):
return net in self._nets
def __iter__(self):
return iter(self._nets)
def __len__(self):
return len(self._nets)
def __getitem__(self, i):
return self._nets[i]
[docs] def add(self, net):
"""Add a net to the collection.
Parameters
----------
net : Net
"""
# We maintain a list of added nets and concatenate it with _data in
# _nets.
self._added_nets.append(net)
@property
def dimension(self):
"""Dimension of the collection.
Only works if all nets in the collection have the same dimension.
Returns
-------
int
Raises
------
AttributeError
* If collection is empty
* If nets in collection have different dimensions.
"""
dims = {n.dimension for n in self._nets}
if len(dims) == 1:
return dims.pop()
elif len(dims) > 1:
raise AttributeError(
"Dimension of NetCollection is not well-defined because it "
f"contains nets of dimensions {dims}."
)
elif len(dims) == 0:
raise AttributeError("NetCollection is empty.")
@property
def _nets(self):
"""Get collection as list.
Returns
-------
list of Net
"""
if not hasattr(self, "_data"):
self._data = self.nets + self._added_nets
return self._data