"""
.. currentmodule:: neet
.. testsetup:: network
from neet.boolean import ECA
from neet.boolean.examples import s_pombe
The :mod:`neet` module provides the following abstract network classes from
which all concrete Neet networks inherit:
.. autosummary::
:nosignatures:
Network
UniformNetwork
.. inheritance-diagram:: neet.Network neet.UniformNetwork
:parts: 1
These classes provide an abstract interface which algorithms can leverage for
generic implementation of various network-theoretic analyses.
"""
from abc import ABCMeta, abstractmethod
from .python import long
from .statespace import StateSpace
from .landscape import LandscapeMixin
import networkx as nx
import six
[docs]@six.add_metaclass(ABCMeta)
class Network(LandscapeMixin, StateSpace):
"""
The Network class is the core base class for all Neet networks. It provides
an interface for describing network state updating and simple
graph-theoretic analyses.
.. autosummary::
:nosignatures:
names
metadata
_unsafe_update
update
neighbors_in
neighbors_out
neighbors
network_graph
draw_network_graph
Network is an *abstract* class, meaning it cannot be instantiated, and
inherits from :class:`neet.LandscapeMixin` and :class:`neet.StateSpace`.
Initialization of the Network requires, at a minimum, a specification of
the shape of the network's state space, and optionally allows the user to
specify a list of names for the nodes of the network and a metadata
dictionary for the network as a whole (e.g. citation information).
Any concrete deriving class must overload the following methods:
* :meth:`_unsafe_update`
* :meth:`neighbors_in`
* :meth:`neighbors_out`
:param shape: the base of each node of the network
:type shape: list
:param names: an iterable object of the names of the nodes in the network
:type names: seq
:param metadata: metadata dictionary for the network
:type metadata: dict
"""
def __init__(self, shape, names=None, metadata=None):
super(Network, self).__init__(shape)
if metadata is None:
metadata = dict()
elif not isinstance(metadata, dict):
raise TypeError('metadata is not a dict')
self._metadata = metadata
self.names = names
@property
def metadata(self):
"""
Any metadata associated with the network.
"""
return self._metadata
@property
def names(self):
"""
Get or set the names of the nodes of the network.
:raises TypeError: if the assigned value is not convertable to a list
:raises ValueError: if the length fo the assigned values does not match the networks's size
"""
return self._names
@names.setter
def names(self, names):
if names is not None:
try:
names = list(names)
except TypeError:
raise TypeError('names must be convertable to a list')
if len(names) != self.size:
raise ValueError('number of names does not match network size')
self._names = names
[docs] @abstractmethod
def _unsafe_update(self, state, index, pin, values, *args, **kwargs):
"""
Unsafely update the state of a network in place.
This function accepts three optional arguments by default:
* ``index`` - update only the specified node (by index)
* ``pin`` - do not update the state of any node in a list
* ``values`` - set the state of some subset of nodes to specified values
.. Note::
As an abstract method, every concrete class derving from Network
must overload this method. The overload **should not** perform no
ensurance checks on the arguments to maximize performance, as those
check are performed in the :meth:`update` method. Further, it is
assumed that this method *modifies* the ``state`` argument in-place
and no others.
:param state: the state of the network to update
:type state: list, numpy.ndarray
:param index: the index to update
:type index: int or None
:param pin: which nodes to pin to their current state
:type pin: list, numpy.ndarray or None
:param values: a dictionary mapping nodes to a state to which to reset the node to
:type values: dict or None
:returns: the updated state
"""
pass
[docs] def update(self, state, index=None, pin=None, values=None, *args, **kwargs):
"""
Update the state of a network in place.
This function accepts three optional arguments by default:
* ``index`` - update only the specified node (by index)
* ``pin`` - do not update the state of any node in a list
* ``values`` - set the state of some subset of nodes to specified values
.. rubric:: Examples
**Updates States In-Place:**
.. doctest:: network
>>> rule = ECA(30, size=5)
>>> state = [0, 0, 1, 0, 0]
>>> rule.update(state)
[0, 1, 1, 1, 0]
>>> state
[0, 1, 1, 1, 0]
**Updating A Single Node:**
.. doctest:: network
>>> rule = ECA(30, size=5)
>>> rule.update([0, 0, 1, 0, 0])
[0, 1, 1, 1, 0]
>>> rule.update([0, 0, 1, 0, 0], index=1)
[0, 1, 1, 0, 0]
**Pinning States:**
.. doctest:: network
>>> rule = ECA(30, size=5)
>>> rule.update([0, 0, 1, 0, 0])
[0, 1, 1, 1, 0]
>>> rule.update([0, 0, 1, 0, 0], pin=[1])
[0, 0, 1, 1, 0]
**Overriding States:**
.. doctest:: network
>>> rule = ECA(30, size=5)
>>> rule.update([0, 0, 1, 0, 0])
[0, 1, 1, 1, 0]
>>> rule.update([0, 0, 1, 0, 0], values={0: 1, 2: 0})
[1, 1, 0, 1, 0]
This function ensures that:
1. If ``index`` is provided, then neither ``pin`` nor ``values`` is
provided.
2. If ``pin`` and ``values`` are both provided, then they do not affect
the same nodes.
3. If ``values`` is provided, then the overriding states specified in
it are consistent with the state space of the network.
.. Note::
Typically, this method should not be overloaded unless the
particular deriving class makes use of the ``args`` or ``kwargs``
arguments. In that case, it should first ensure that those
arguments are well-behaved, and and the delegate subsequent checks
and the call to :meth:`_unsafe_update` to a call to this
:meth:`neet.Network.update`.
:param state: the state of the network to update
:type state: list or numpy.ndarray
:param index: the index to update
:type index: int or None
:param pin: which nodes to pin to their current state
:type pin: list, numpy.ndarray or None
:param values: a dictionary mapping nodes to a state to which to reset the node to
:type values: dict or None
:returns: the updated state
"""
if state not in self:
raise ValueError("the provided state is not in the network's state space")
if index is not None:
if index < 0 or index >= self.size:
raise IndexError("index out of range")
elif pin is not None and pin != []:
raise ValueError("cannot provide both the index and pin arguments")
elif values is not None and values != {}:
raise ValueError("cannot provide both the index and values arguments")
elif pin is not None and values is not None:
for k in values.keys():
if k in pin:
raise ValueError("cannot set a value for a pinned state")
if values is not None:
bases = self.shape
for key in values.keys():
val = values[key]
if val < 0 or val >= bases[key]:
raise ValueError("invalid state in values argument")
return self._unsafe_update(state, index, pin, values, *args, **kwargs)
[docs] @abstractmethod
def neighbors_in(self, index, *args, **kwargs):
"""
Get a set of all incoming neighbors of the node at ``index``.
All concrete network classes must overload this method.
:param index: the index of the node target node
:type index: int
:returns: a set of incoming neighbor indices
"""
pass
[docs] @abstractmethod
def neighbors_out(self, index, *args, **kwargs):
"""
Get a set of all outgoing neighbors of the node at ``index``.
All concrete network classes must overload this method.
:param index: the index of the node source node
:type index: int
:returns: a set of outgoing neighbor indices
"""
pass
[docs] def neighbors(self, index, direction='both', *args, **kwargs):
"""
Get a set of the neighbors of the node at ``index``. Optionally,
specify the directionality of the neighboring edges, e.g. ``'in'``,
``'out'`` or ``'both'``.
.. rubric:: Examples
**All Neighbors:**
.. doctest:: network
>>> s_pombe.neighbors(7)
{1, 5, 7, 8}
**Incoming Neighbors:**
.. doctest:: network
>>> s_pombe.neighbors(7, direction='in')
{8, 1, 7}
**Outgoing Neighbors:**
.. doctest:: network
>>> s_pombe.neighbors(7, direction='out')
{5, 7}
:param index: the index of the node
:type index: int
:param direction: the directionality of the neighboring edges
:type direction: str
:returns: a set of neighboring node indices, respecting ``direction``.
"""
if direction not in ('in', 'out', 'both'):
raise ValueError('direction must be "in", "out" or "both"')
if direction == 'in':
return self.neighbors_in(index, *args, **kwargs)
elif direction == 'out':
return self.neighbors_out(index, *args, **kwargs)
else:
inputs = self.neighbors_in(index, *args, **kwargs)
outputs = self.neighbors_out(index, *args, **kwargs)
return inputs.union(outputs)
[docs] def network_graph(self, labels='indices', **kwargs):
"""
The graph of the network as a :class:`networkx.DiGraph`.
This method should only be overloaded by derived classes if additional
metadata is to be added to the graph by default.
.. rubric:: Examples
.. doctest:: network
>>> s_pombe.network_graph()
<networkx.classes.digraph.DiGraph object at 0x...>
:param labels: label to be applied to graph nodes (either ``'indices'`` or ``'names'``)
:param kwargs: kwargs to pass to the :class:`networkx.DiGraph` constructor
:return: a :class:`networkx.DiGraph` object
"""
if labels == 'indices':
edges = [(i, j) for i in range(self.size) for j in self.neighbors_out(i)]
elif labels == 'names' and self.names is not None:
names = self.names
edges = [(names[i], names[j]) for i in range(self.size) for j in self.neighbors_out(i)]
elif labels == 'names' and self.names is None:
raise ValueError("network nodes do not have names")
else:
raise ValueError("labels argument must be 'names' or 'indices', got {}".format(labels))
kwargs.update(self.metadata)
return nx.DiGraph(edges, **kwargs)
[docs] def draw_network_graph(self, graphkwargs={}, pygraphkwargs={}):
"""
Draw network's networkx graph using PyGraphviz.
.. Note::
This method requires `Graphviz <https://graphviz.org/>`_ and
`pygraphviz <https://pypi.org/project/pygraphviz/>`_. The former
requires manual installation (see
https://graphviz.gitlab.io/download/), while the latter can be
installed via ``pip``.
:param graphkwargs: kwargs to pass to :meth:`network_graph`
:param pygraphkwargs: kwargs to pass to :func:`neet.draw.view_pygraphviz`
"""
from .draw import view_pygraphviz
default_args = {'prog': 'circo'}
graph = self.network_graph(**graphkwargs)
view_pygraphviz(graph, **dict(default_args, **pygraphkwargs))
Network.register(UniformNetwork)