#!/usr/bin/env python
#-*- coding:utf-8 -*-
#
# This file is part of the NNGT project to generate and analyze
# neuronal networks and their activity.
# Copyright (C) 2015-2017 Tanguy Fardet
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Graph classes for graph generation and management """
from copy import deepcopy
import logging
import weakref
import numpy as np
import scipy.sparse as ssp
import nngt
from nngt import save_to_file
import nngt.analysis as na
from nngt.lib import (InvalidArgument, nonstring_container, default_neuron,
default_synapse, POS, WEIGHT, DELAY, DIST, TYPE)
from nngt.lib.graph_helpers import _edge_prop
from nngt.lib.io_tools import _as_string, _load_from_file
from nngt.lib.logger import _log_message
from nngt.lib.test_functions import graph_tool_check, deprecated
if nngt._config['with_nest']:
from nngt.simulation import make_nest_network
__all__ = ['Graph', 'SpatialGraph', 'Network', 'SpatialNetwork']
logger = logging.getLogger(__name__)
# ----- #
# Graph #
# ----- #
[docs]class Graph(nngt.core.GraphObject):
"""
The basic graph class, which inherits from a library class such as
:class:`gt.Graph`, :class:`networkx.DiGraph`, or `igraph.Graph`.
The objects provides several functions to easily access some basic
properties.
"""
#-------------------------------------------------------------------------#
# Class properties
__num_graphs = 0
__max_id = 0
[docs] @classmethod
def num_graphs(cls):
''' Returns the number of alive instances. '''
return cls.__num_graphs
@classmethod
def from_library(cls, library_graph, weighted=True, directed=True,
**kwargs):
library_graph = nngt.core.GraphObject.to_graph_object(library_graph)
library_graph.__class__ = cls
if weighted:
library_graph._w = _edge_prop("weights", kwargs)
library_graph._d = _edge_prop("delays", kwargs)
library_graph.__id = cls.__max_id
library_graph._name = "Graph" + str(cls.__num_graphs)
cls.__max_id += 1
cls.__num_graphs += 1
return library_graph
[docs] @classmethod
def from_matrix(cls, matrix, weighted=True, directed=True):
'''
Creates a :class:`~nngt.Graph` from a :class:`scipy.sparse` matrix or
a dense matrix.
Parameters
----------
matrix : :class:`scipy.sparse` matrix or :class:`numpy.array`
Adjacency matrix.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
Returns
-------
:class:`~nngt.Graph`
'''
shape = matrix.shape
graph_name = "FromYMatrix_Z"
nodes = max(shape[0], shape[1])
if issubclass(matrix.__class__, ssp.spmatrix):
graph_name = graph_name.replace('Y', 'Sparse')
if not directed:
if shape[0] != shape[1] or not (matrix.T != matrix).nnz == 0:
raise InvalidArgument('Incompatible `directed=False` '
'option provided for non symmetric '
'matrix.')
else:
graph_name = graph_name.replace('Y', 'Dense')
if not directed:
if shape[0] != shape[1] or not (matrix.T == matrix).all():
raise InvalidArgument('Incompatible `directed=False` '
'option provided for non symmetric '
'matrix.')
edges = np.array(matrix.nonzero()).T
graph = cls(nodes, name=graph_name.replace("Z", str(cls.__num_graphs)),
weighted=weighted, directed=directed)
weights = None
if weighted:
if issubclass(matrix.__class__, ssp.spmatrix):
weights = np.array(matrix[edges[:, 0], edges[:, 1]])[0]
else:
weights = matrix[edges[:, 0], edges[:, 1]]
graph.new_edges(edges, {"weight": weights}, check_edges=False)
return graph
[docs] @staticmethod
@graph_tool_check('2.22')
def from_file(filename, fmt="auto", separator=" ", secondary=";",
attributes=None, notifier="@", ignore="#",
from_string=False):
'''
Import a saved graph from a file.
@todo: implement population and shape loading, implement gml, dot, xml,
gt
Parameters
----------
filename: str
The path to the file.
fmt : str, optional (default: "neighbour")
The format used to save the graph. Supported formats are:
"neighbour" (neighbour list, default if format cannot be deduced
automatically), "ssp" (scipy.sparse), "edge_list" (list of all the
edges in the graph, one edge per line, represented by a ``source
target``-pair), "gml" (gml format, default if `filename` ends with
'.gml'), "graphml" (graphml format, default if `filename` ends
with '.graphml' or '.xml'), "dot" (dot format, default if
`filename` ends with '.dot'), "gt" (only when using
`graph_tool`<http://graph-tool.skewed.de/>_ as library, detected
if `filename` ends with '.gt').
separator : str, optional (default " ")
separator used to separate inputs in the case of custom formats
(namely "neighbour" and "edge_list")
secondary : str, optional (default: ";")
Secondary separator used to separate attributes in the case of
custom formats.
attributes : list, optional (default: [])
List of names for the attributes present in the file. If a
`notifier` is present in the file, names will be deduced from it;
otherwise the attributes will be numbered.
This argument can also be used to load only a subset of the saved
attributes.
notifier : str, optional (default: "@")
Symbol specifying the following as meaningfull information.
Relevant information is formatted ``@info_name=info_value``, where
``info_name`` is in ("attributes", "directed", "name", "size") and
associated ``info_value``s are of type (``list``, ``bool``,
``str``, ``int``).
Additional notifiers are ``@type=SpatialGraph/Network/
SpatialNetwork``, which must be followed by the relevant notifiers
among ``@shape``, ``@population``, and ``@graph``.
from_string : bool, optional (default: False)
Load from a string instead of a file.
Returns
-------
graph : :class:`~nngt.Graph` or subclass
Loaded graph.
'''
info, edges, nattr, eattr, pop, shape, pos = _load_from_file(
filename=filename, fmt=fmt, separator=separator,
secondary=secondary, attributes=attributes, notifier=notifier)
# create the graph
graph = Graph(nodes=info["size"], name=info["name"],
directed=info["directed"])
# make the nodes attributes
lst_attr, dtpes, lst_values = [], [], []
if info["node_attributes"]: # edge attributes to add to the graph
lst_attr = info["node_attributes"]
dtpes = info["node_attr_types"]
lst_values = [nattr[name] for name in info["node_attributes"]]
for nattr, dtype, values in zip(lst_attr, dtpes, lst_values):
graph.new_node_attribute(nattr, dtype, values=values)
# make the edges and their attributes
lst_attr, dtpes, lst_values = [], [], []
if info["edge_attributes"]: # edge attributes to add to the graph
lst_attr = info["edge_attributes"]
dtpes = info["edge_attr_types"]
lst_values = [eattr[name] for name in info["edge_attributes"]]
graph.new_edges(edges, check_edges=False)
for eattr, dtype, values in zip(lst_attr, dtpes, lst_values):
graph.new_edge_attribute(eattr, dtype, values=values)
if pop is not None:
Network.make_network(graph, pop)
pop._parent = weakref.ref(graph)
for g in pop.values():
g._pop = weakref.ref(pop)
g._net = weakref.ref(graph)
if pos is not None or shape is not None:
SpatialGraph.make_spatial(graph, shape=shape, positions=pos)
return graph
[docs] @staticmethod
def make_spatial(graph, shape=None, positions=None, copy=False):
'''
Turn a :class:`~nngt.Graph` object into a :class:`~nngt.SpatialGraph`,
or a :class:`~nngt.Network` into a :class:`~nngt.SpatialNetwork`.
Parameters
----------
graph : :class:`~nngt.Graph` or :class:`~nngt.SpatialGraph`
Graph to convert.
shape : :class:`~nngt.geometry.Shape`, optional (default: None)
Shape to associate to the new :class:`~nngt.SpatialGraph`.
positions : (N, 2) array
Positions, in a 2D space, of the N neurons.
copy : bool, optional (default: ``False``)
Whether the operation should be made in-place on the object or if a
new object should be returned.
Notes
-----
In-place operation that directly converts the original graph if `copy`
is ``False``, else returns the copied :class:`~nngt.Graph` turned into
a :class:`~nngt.SpatialGraph`.
The `shape` argument can be skipped if `positions` are given; in that
case, the neurons will be embedded in a rectangle that contains them
all.
'''
if copy:
graph = graph.copy()
if isinstance(graph, Network):
graph.__class__ = SpatialNetwork
else:
graph.__class__ = SpatialGraph
graph._init_spatial_properties(shape, positions)
if copy:
return graph
[docs] @staticmethod
def make_network(graph, neural_pop, copy=False, **kwargs):
'''
Turn a :class:`~nngt.Graph` object into a :class:`~nngt.Network`, or a
:class:`~nngt.SpatialGraph` into a :class:`~nngt.SpatialNetwork`.
Parameters
----------
graph : :class:`~nngt.Graph` or :class:`~nngt.SpatialGraph`
Graph to convert
neural_pop : :class:`~nngt.NeuralPop`
Population to associate to the new :class:`~nngt.Network`
copy : bool, optional (default: ``False``)
Whether the operation should be made in-place on the object or if a
new object should be returned.
Notes
-----
In-place operation that directly converts the original graph if `copy`
is ``False``, else returns the copied :class:`~nngt.Graph` turned into
a :class:`~nngt.Network`.
'''
if copy:
graph = graph.copy()
if isinstance(graph, SpatialGraph):
graph.__class__ = SpatialNetwork
else:
graph.__class__ = Network
# set delays to 1. or to provided value if they are not already set
if "delays" not in kwargs and not hasattr(graph, '_d'):
graph._d = {"distribution": "constant", "value": 1.}
elif "delays" in kwargs and not hasattr(graph, '_d'):
graph._d = kwargs["delays"]
elif "delays" in kwargs:
_log_message(logger, "WARNING",
'Graph already had delays set, ignoring new ones.')
graph._init_bioproperties(neural_pop)
if copy:
return graph
#-------------------------------------------------------------------------#
# Constructor/destructor and properties
def __init__(self, nodes=0, name="Graph", weighted=True, directed=True,
from_graph=None, **kwargs):
'''
Initialize Graph instance
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
from_graph : :class:`~nngt.core.GraphObject`, optional
An optional :class:`~nngt.core.GraphObject` to serve as base.
kwargs : optional keywords arguments
Optional arguments that can be passed to the graph, e.g. a dict
containing information on the synaptic weights
(``weights={"distribution": "constant", "value": 2.3}`` which is
equivalent to ``weights=2.3``), the synaptic `delays`, or a
``type`` information.
Returns
-------
self : :class:`~nngt.Graph`
'''
self.__id = self.__class__.__max_id
self._name = name
self._graph_type = kwargs["type"] if "type" in kwargs else "custom"
# Init the core.GraphObject
super(Graph, self).__init__(nodes=nodes, g=from_graph,
directed=directed, weighted=weighted)
# take care of the weights and delays
# @todo: use those of the from_graph
if weighted:
self.new_edge_attribute('weight', 'double')
self._w = _edge_prop(kwargs.get("weights", None))
if "delays" in kwargs:
self.new_edge_attribute('delay', 'double')
self._d = _edge_prop(kwargs.get("delays", None))
if 'inh_weight_factor' in kwargs:
self._iwf = kwargs['inh_weight_factor']
# update the counters
self.__class__.__num_graphs += 1
self.__class__.__max_id += 1
def __del__(self):
self.__class__.__num_graphs -= 1
def __repr__(self):
''' Provide unambiguous informations regarding the object. '''
d = "directed" if self._directed else "undirected"
w = "weighted" if self._weighted else "binary"
t = self.type
n = self.node_nb()
e = self.edge_nb()
return "<{directed}/{weighted} {obj} object of type '{net_type}' " \
"with {nodes} nodes and {edges} edges at 0x{obj_id}>".format(
directed=d, weighted=w, obj=type(self).__name__,
net_type=t, nodes=n, edges=e, obj_id=id(self))
def __str__(self):
'''
Return the full string description of the object as would be stored
inside a file when saving the graph.
'''
return _as_string(self)
@property
def graph_id(self):
''' Unique :class:`int` identifying the instance. '''
return self.__id
@property
def name(self):
''' Name of the graph. '''
return self._name
@property
def type(self):
''' Type of the graph. '''
return self._graph_type
#-------------------------------------------------------------------------#
# Graph actions
[docs] def copy(self):
'''
Returns a deepcopy of the current :class:`~nngt.Graph`
instance
'''
gc_instance = Graph(name=self._name + '_copy',
weighted=self._weighted,
from_graph=self)
if self.is_spatial():
SpatialGraph.make_spatial(gc_instance)
if self.is_network():
Network.make_network(gc_instance, deepcopy(self.population))
return gc_instance
[docs] @graph_tool_check('2.22')
def to_file(self, filename, fmt="auto", separator=" ", secondary=";",
attributes=None, notifier="@"):
'''
Save graph to file; options detailed below.
.. seealso::
:py:func:`nngt.lib.save_to_file` function for options.
'''
save_to_file(self, filename, fmt=fmt, separator=separator,
secondary=secondary, attributes=attributes,
notifier=notifier)
#~ def inhibitory_subgraph(self):
#~ ''' Create a :class:`~nngt.Graph` instance which graph
#~ contains only the inhibitory edges of the current instance's
#~ :class:`graph_tool.Graph` '''
#~ eprop_b_type = self._graph.new_edge_property(
#~ "bool",-self._graph.edge_properties[TYPE].a+1)
#~ self._graph.set_edge_filter(eprop_b_type)
#~ inhib_graph = Graph( name=self._name + '_inhib',
#~ weighted=self._weighted,
#~ from_graph=core.GraphObject(self._graph,prune=True) )
#~ self.clear_filters()
#~ return inhib_graph
#~ def excitatory_subgraph(self):
#~ '''
#~ Create a :class:`~nngt.Graph` instance which graph contains only the
#~ excitatory edges of the current instance's :class:`core.GraphObject`.
#~ .. warning ::
#~ Only works for graph_tool
#~ .. todo ::
#~ Make this method library independant!
#~ '''
#~ eprop_b_type = self._graph.new_edge_property(
#~ "bool",self._graph.edge_properties[TYPE].a+1)
#~ self._graph.set_edge_filter(eprop_b_type)
#~ exc_graph = Graph( name=self._name + '_exc',
#~ weighted=self._weighted,
#~ graph=core.GraphObject(self._graph,prune=True) )
#~ self._graph.clear_filters()
#~ return exc_graph
#-------------------------------------------------------------------------#
# Setters
[docs] def set_name(self, name=""):
''' set graph name '''
if name != "":
self._name = name
else:
self._name = "Graph_" + str(self.__id)
[docs] def new_edge_attribute(self, name, value_type, values=None, val=None):
'''
Create a new attribute for the edges.
.. versionadded:: 0.7
Parameters
----------
name : str
The name of the new attribute.
value_type : str
Type of the attribute, among 'int', 'double', 'string'
values : array, optional (default: None)
Values with which the edge attribute should be initialized.
(must have one entry per node in the graph)
val : int, float or str , optional (default: None)
Identical value for all edges.
'''
self._eattr.new_attribute(name, value_type, values=values, val=val)
[docs] def new_node_attribute(self, name, value_type, values=None, val=None):
'''
Create a new attribute for the nodes.
.. versionadded:: 0.7
Parameters
----------
name : str
The name of the new attribute.
value_type : str
Type of the attribute, among 'int', 'double', 'string'
values : array, optional (default: None)
Values with which the node attribute should be initialized.
(must have one entry per node in the graph)
val : int, float or str , optional (default: None)
Identical value for all nodes.
'''
self._nattr.new_attribute(name, value_type, values=values, val=val)
[docs] def set_edge_attribute(self, attribute, values=None, val=None,
value_type=None, edges=None):
'''
Set attributes to the connections between neurons.
.. warning ::
The special "type" attribute cannot be modified when using graphs
that inherit from the :class:`~nngt.Network` class. This is because
for biological networks, neurons make only one kind of synapse,
which is determined by the :class:`nngt.NeuralGroup` they
belong to.
Parameters
----------
attribute : str
The name of the attribute.
value_type : str
Type of the attribute, among 'int', 'double', 'string'
values : array, optional (default: None)
Values with which the edge attribute should be initialized.
(must have one entry per node in the graph)
val : int, float or str , optional (default: None)
Identical value for all edges.
value_type : str, optional (default: None)
Type of the attribute, among 'int', 'double', 'string'. Only used
if the attribute does not exist and must be created.
edges : list of edges or array of shape (E, 2), optional (default: all)
Edges whose attributes should be set. Others will remain unchanged.
'''
if attribute not in self.edges_attributes:
assert value_type is not None, "`value_type` is necessary for " +\
"new attributes."
self.new_edge_attribute(name=attribute, value_type=value_type,
values=values, val=val)
else:
num_edges = self.edge_nb() if edges is None else len(edges)
if values is None:
if val is not None:
values = np.repeat(val, num_edges)
else:
raise InvalidArgument("At least one of the `values` and "
"`val` arguments should not be ``None``.")
self._eattr.set_attribute(attribute, values, edges=edges)
[docs] def set_node_attribute(self, attribute, values=None, val=None,
value_type=None, nodes=None):
'''
Set attributes to the connections between neurons.
.. versionadded:: 0.9
Parameters
----------
attribute : str
The name of the attribute.
value_type : str
Type of the attribute, among 'int', 'double', 'string'
values : array, optional (default: None)
Values with which the edge attribute should be initialized.
(must have one entry per node in the graph)
val : int, float or str , optional (default: None)
Identical value for all edges.
value_type : str, optional (default: None)
Type of the attribute, among 'int', 'double', 'string'. Only used
if the attribute does not exist and must be created.
nodes : list of nodes, optional (default: all)
Nodes whose attributes should be set. Others will remain unchanged.
'''
if attribute not in self.nodes_attributes:
assert value_type is not None, "`value_type` is necessary for " +\
"new attributes."
self.new_node_attribute(name=attribute, value_type=value_type,
values=values, val=val)
else:
num_nodes = self.node_nb() if nodes is None else len(nodes)
if values is None:
if val is not None:
values = np.repeat(val, num_nodes)
else:
raise InvalidArgument("At least one of the `values` and "
"`val` arguments should not be ``None``.")
self._nattr.set_attribute(attribute, values, nodes=nodes)
[docs] def set_weights(self, weight=None, elist=None, distribution=None,
parameters=None, noise_scale=None):
'''
Set the synaptic weights.
..todo ::
take elist into account in Connections.weights
Parameters
----------
weight : float or class:`numpy.array`, optional (default: None)
Value or list of the weights (for user defined weights).
elist : class:`numpy.array`, optional (default: None)
List of the edges (for user defined weights).
distribution : class:`string`, optional (default: None)
Type of distribution (choose among "constant", "uniform",
"gaussian", "lognormal", "lin_corr", "log_corr").
parameters : dict, optional (default: {})
Dictionary containing the properties of the weight distribution.
Properties are as follow for the distributions
- 'constant': 'value'
- 'uniform': 'lower', 'upper'
- 'gaussian': 'avg', 'std'
- 'lognormal': 'position', 'scale'
noise_scale : class:`int`, optional (default: None)
Scale of the multiplicative Gaussian noise that should be applied
on the weights.
'''
if isinstance(weight, float):
size = self.edge_nb() if elist is None else len(elist)
self._w = {"distribution": "constant", "value": weight}
weight = np.repeat(weight, size)
elif not nonstring_container(weight) and weight is not None:
raise AttributeError("Invalid `weight` value: must be either "
"float, array-like or None.")
elif weight is not None:
self._w = {"distribution": "custom"}
elif None not in (distribution, parameters):
self._w = {"distribution": distribution}
self._w.update(parameters)
if distribution is None:
distribution = self._w["distribution"]
if parameters is None:
parameters = self._w
nngt.core.Connections.weights(
self, elist=elist, wlist=weight, distribution=distribution,
parameters=parameters, noise_scale=noise_scale)
[docs] def set_types(self, syn_type, nodes=None, fraction=None):
'''
Set the synaptic/connection types.
.. warning ::
The special "type" attribute cannot be modified when using graphs
that inherit from the :class:`~nngt.Network` class. This is because
for biological networks, neurons make only one kind of synapse,
which is determined by the :class:`nngt.NeuralGroup` they
belong to.
Parameters
----------
syn_type : int or string
Type of the connection among 'excitatory' (also `1`) or
'inhibitory' (also `-1`).
nodes : int, float or list, optional (default: `None`)
If `nodes` is an int, number of nodes of the required type that
will be created in the graph (all connections from inhibitory nodes
are inhibitory); if it is a float, ratio of `syn_type` nodes in the
graph; if it is a list, ids of the `syn_type` nodes.
fraction : float, optional (default: `None`)
Fraction of the selected edges that will be set as `syn_type` (if
`nodes` is not `None`, it is the fraction of the specified nodes'
edges, otherwise it is the fraction of all edges in the graph).
Returns
-------
t_list : :class:`numpy.ndarray`
List of the types in an order that matches the `edges` attribute of
the graph.
'''
inhib_nodes = nodes
if syn_type == 'excitatory' or syn_type == 1:
if is_integer(nodes):
inhib_nodes = graph.node_nb() - nodes
elif isinstance(nodes, np.float):
inhib_nodes = 1. / nodes
elif nonstring_container(nodes):
inhib_nodes = list(range(graph.node_nb()))
nodes.sort()
for node in nodes[::-1]:
del inhib_nodes[node]
return nngt.core.Connections.types(self, inhib_nodes, fraction)
[docs] def set_delays(self, delay=None, elist=None, distribution=None,
parameters=None, noise_scale=None):
'''
Set the delay for spike propagation between neurons.
..todo ::
take elist into account in Connections.delays
Parameters
----------
delay : float or class:`numpy.array`, optional (default: None)
Value or list of delays (for user defined delays).
elist : class:`numpy.array`, optional (default: None)
List of the edges (for user defined delays).
distribution : class:`string`, optional (default: None)
Type of distribution (choose among "constant", "uniform",
"gaussian", "lognormal", "lin_corr", "log_corr").
parameters : dict, optional (default: {})
Dictionary containing the properties of the delay distribution.
noise_scale : class:`int`, optional (default: None)
Scale of the multiplicative Gaussian noise that should be applied
on the delays.
'''
# check special cases and set self._d
if isinstance(delay, float):
size = self.edge_nb() if elist is None else len(elist)
self._d = {"distribution": "constant", "value": delay}
delay = np.repeat(delay, size)
elif not nonstring_container(delay) and delay is not None:
raise AttributeError("Invalid `delay` value: must be either "
"float, array-like or None")
elif delay is not None:
self._d = {"distribution": "custom"}
elif None not in (distribution, parameters):
self._d = {"distribution": distribution}
self._d.update(parameters)
if distribution is None:
if hasattr(self, "_d"):
distribution = self._d["distribution"]
else:
raise AttributeError(
"Invalid `distribution` value: cannot be None if "
"default delays were not set at graph creation.")
if parameters is None:
if hasattr(self, "_d"):
parameters = self._d
else:
raise AttributeError(
"Invalid `parameters` value: cannot be None if default"
" delays were not set at graph creation.")
return nngt.core.Connections.delays(
self, elist=elist, dlist=delay, distribution=distribution,
parameters=parameters, noise_scale=noise_scale)
#-------------------------------------------------------------------------#
# Getters
@property
def nodes_attributes(self):
'''
Access node attributes
.. versionadded:: 0.7
'''
return self._nattr
@property
def edges_attributes(self):
'''
Access edge attributes
.. versionadded:: 0.7
'''
return self._eattr
[docs] def get_edge_attributes(self, edges=None, name=None):
'''
Attributes of the graph's edges.
.. versionchanged:: 1.0
Returns the full dict of edges attributes if called without
arguments.
.. versionadded:: 0.8
Parameters
----------
edge : tuple or list of tuples, optional (default: ``None``)
Edge whose attribute should be displayed.
name : str, optional (default: ``None``)
Name of the desired attribute.
Returns
-------
Dict containing all graph's attributes (synaptic weights, delays...)
by default. If `edge` is specified, returns only the values for these
edges. If `name` is specified, returns value of the attribute for each
edge.
Note
----
The attributes values are ordered as the edges in
:func:`~nngt.Graph.edges_array`.
'''
if name is not None and edges is not None:
if isinstance(edges, slice):
return self._eattr[name][edges]
else:
return self._eattr[edges][name]
elif name is None and edges is None:
return {k: self._eattr[k] for k in self._eattr.keys()}
elif name is None:
return self._eattr[edges]
else:
return self._eattr[name]
[docs] def get_node_attributes(self, nodes=None, name=None):
'''
Attributes of the graph's edges.
.. versionchanged:: 1.0.1
Corrected default behavior and made it the same as
:func:`~nngt.Graph.get_edge_attributes`.
.. versionadded:: 0.9
Parameters
----------
nodes : list of ints, optional (default: ``None``)
Nodes whose attribute should be displayed.
name : str, optional (default: ``None``)
Name of the desired attribute.
Returns
-------
Dict containing all nodes attributes by default. If `nodes` is
specified, returns a ``dict`` containing only the attributes of these
nodes. If `name` is specified, returns a list containing the values of
the specific attribute for the required nodes (or all nodes if
unspecified).
'''
res = None
if name is None:
res = {k: self._nattr[k] for k in self._nattr.keys()}
else:
res = self._nattr[name]
if nodes is None:
return res
else:
if isinstance(nodes, (slice, int)) or nonstring_container(nodes):
return res[nodes]
else:
raise ValueError("Invalid `nodes`: "
"{}, use slice, int, or list".format(nodes))
[docs] def get_attribute_type(self, attribute_name, attribute_class=None):
'''
Return the type of an attribute (e.g. string, double, int).
.. versionchanged:: 1.0
Added `attribute_class` parameter.
Parameters
----------
attribute_name : str
Name of the attribute.
attribute_class : str, optional (default: both)
Whether `attribute_name` is a "node" or an "edge" attribute.
Returns
-------
type : str
Type of the attribute.
'''
if attribute_class is None:
if attribute_name in self._eattr and attribute_name in self._nattr:
raise RuntimeError("Both edge and node attributes with name '"
+ attribute_name + "' exist, please "
"specify `attribute_class`")
elif attribute_name in self._eattr:
return self._eattr.value_type(attribute_name)
elif attribute_name in self._nattr:
return self._nattr.value_type(attribute_name)
else:
raise KeyError("No '{}' attribute.".format(attribute_name))
else:
if attribute_class == "edge":
return self._eattr.value_type(attribute_name)
elif attribute_class == "node":
return self._nattr.value_type(attribute_name)
else:
raise InvalidArgument(
"Unknown attribute class '{}'.".format(attribute_class))
[docs] def get_name(self):
''' Get the name of the graph '''
return self._name
[docs] def get_graph_type(self):
''' Return the type of the graph (see nngt.generation) '''
return self._graph_type
[docs] def get_density(self):
'''
Density of the graph: :math:`\\frac{E}{N^2}`, where `E` is the number of
edges and `N` the number of nodes.
'''
return self.edge_nb()/float(self.node_nb()**2)
[docs] def is_weighted(self):
''' Whether the edges have weights '''
return "weight" in self.edges_attributes
[docs] def is_directed(self):
''' Whether the graph is directed or not '''
return self._directed
[docs] def get_degrees(self, deg_type="total", node_list=None, use_weights=False,
syn_type="all"):
'''
Degree sequence of all the nodes.
..versionchanged :: 0.9
Added `syn_type` keyword.
Parameters
----------
deg_type : string, optional (default: "total")
Degree type (among 'in', 'out' or 'total').
node_list : list, optional (default: None)
List of the nodes which degree should be returned
use_weights : bool, optional (default: False)
Whether to use weighted (True) or simple degrees (False).
syn_type : int or str, optional (default: all)
Restrict to a given synaptic type ("excitatory", 1, or
"inhibitory", -1).
Returns
-------
:class:`numpy.array` or None (if an invalid type is asked).
'''
valid_types = ("in", "out", "total")
if deg_type in valid_types:
if syn_type in ("excitatory", 1):
e_neurons = []
if isinstance(self, Network):
for g in self.population.values():
if g.neuron_type == 1:
e_neurons.extend(g.ids)
else:
e_neurons = np.where(
self.get_node_attributes(name="type") == 1)[0]
return self.adjacency_matrix(
weights=use_weights,
types=False)[e_neurons, :].sum(axis=0).A1
elif syn_type in ("inhibitory", -1):
i_neurons = []
if isinstance(self, Network):
for g in self.population.values():
if g.neuron_type == -1:
i_neurons.extend(g.ids)
else:
i_neurons = np.where(
self.get_node_attributes(name="type") == -1)[0]
return self.adjacency_matrix(
weights=use_weights,
types=False)[i_neurons, :].sum(axis=0).A1
elif syn_type == "all":
return self.degree_list(node_list, deg_type, use_weights)
else:
raise InvalidArgument(
"Invalid synaptic type '{}'".format(syn_type))
else:
raise InvalidArgument("Invalid degree type '{}'".format(deg_type))
[docs] def get_betweenness(self, btype="both", use_weights=False):
'''
Betweenness centrality sequence of all nodes and edges.
Parameters
----------
btype : str, optional (default: ``"both"``)
Type of betweenness to return (``"edge"``, ``"node"``-betweenness,
or ``"both"``).
use_weights : bool, optional (default: False)
Whether to use weighted (True) or simple degrees (False).
Returns
-------
node_betweenness : :class:`numpy.array`
Betweenness of the nodes (if `btype` is ``"node"`` or ``"both"``).
edge_betweenness : :class:`numpy.array`
Betweenness of the edges (if `btype` is ``"edge"`` or ``"both"``).
'''
return self.betweenness_list(btype=btype, use_weights=use_weights)
[docs] def get_edge_types(self, edges=None):
'''
Return the type of all or a subset of the edges.
.. versionchanged:: 1.0.1
Added the possibility to ask for a subset of edges.
Parameters
----------
edges : (E, 2) array, optional (default: all edges)
Edges for which the type should be returned.
Returns
-------
the list of types (1 for excitatory, -1 for inhibitory)
'''
if TYPE in self.edges_attributes:
return self.get_edge_attributes(name=TYPE, edges=edges)
else:
size = self.edge_nb() if edges is None else len(edges)
return np.ones(size)
[docs] def get_weights(self, edges=None):
'''
Returns the weights of all or a subset of the edges.
.. versionchanged:: 1.0.1
Added the possibility to ask for a subset of edges.
Parameters
----------
edges : (E, 2) array, optional (default: all edges)
Edges for which the type should be returned.
Returns
-------
the list of weights
'''
if self.is_weighted():
if edges is None:
return self._eattr["weight"]
else:
return self._eattr[edges]["weight"]
else:
size = self.edge_nb() if edges is None else len(edges)
return np.ones(size)
[docs] def get_delays(self, edges=None):
'''
Returns the delays of all or a subset of the edges.
.. versionchanged:: 1.0.1
Added the possibility to ask for a subset of edges.
Parameters
----------
edges : (E, 2) array, optional (default: all edges)
Edges for which the type should be returned.
Returns
-------
the list of delays
'''
if edges is None:
return self._eattr["delay"]
else:
return self._eattr[edges]["delay"]
[docs] def is_spatial(self):
'''
Whether the graph is embedded in space (i.e. if it has a
:class:`~nngt.geometry.Shape` attribute).
Returns ``True`` is the graph is a subclass of
:class:`~nngt.SpatialGraph`.
'''
return True if issubclass(self.__class__, SpatialGraph) else False
[docs] def is_network(self):
'''
Whether the graph is a subclass of :class:`~nngt.Network` (i.e. if it
has a :class:`~nngt.NeuralPop` attribute).
'''
return True if issubclass(self.__class__, Network) else False
# ------------ #
# SpatialGraph #
# ------------ #
[docs]class SpatialGraph(Graph):
"""
The detailed class that inherits from :class:`Graph` and implements
additional properties to describe spatial graphs (i.e. graph where the
structure is embedded in space.
"""
#-------------------------------------------------------------------------#
# Class properties
__num_graphs = 0
__max_id = 0
#-------------------------------------------------------------------------#
# Constructor, destructor, attributes
def __init__(self, nodes=0, name="Graph", weighted=True, directed=True,
from_graph=None, shape=None, positions=None, **kwargs):
'''
Initialize SpatialClass instance.
.. todo::
see what we do with the from_graph argument
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
shape : :class:`~nngt.geometry.Shape`, optional (default: None)
Shape of the neurons' environment (None leads to a square of
side 1 cm)
positions : :class:`numpy.array` (N, 2), optional (default: None)
Positions of the neurons; if not specified and `nodes` is not 0,
then neurons will be reparted at random inside the
:class:`~nngt.geometry.Shape` object of the instance.
**kwargs : keyword arguments for :class:`~nngt.Graph` or
:class:`~nngt.geometry.Shape` if no shape was given.
Returns
-------
self : :class:`~nggt.SpatialGraph`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_graphs += 1
self.__class__.__max_id += 1
self._shape = None
self._pos = None
super(SpatialGraph, self).__init__(nodes, name, weighted, directed,
from_graph, **kwargs)
self._init_spatial_properties(shape, positions, **kwargs)
if "population" in kwargs:
self.make_network(self, kwargs["population"])
def __del__(self):
if hasattr(self, '_shape'):
if self._shape is not None:
self._shape._parent = None
self._shape = None
super(SpatialGraph, self).__del__()
self.__class__.__num_graphs -= 1
@property
def shape(self):
return self._shape
#-------------------------------------------------------------------------#
# Init tool
def _init_spatial_properties(self, shape, positions=None, **kwargs):
'''
Create the positions of the neurons from the graph `shape` attribute
and computes the connections distances.
'''
self.new_edge_attribute('distance', 'double')
if positions is not None and positions.shape[0] != self.node_nb():
raise InvalidArgument("Wrong number of neurons in `positions`.")
if shape is not None:
shape.set_parent(self)
self._shape = shape
else:
if positions is None:
if 'height' in kwargs and 'width' in kwargs:
self._shape = nngt.geometry.Shape.rectangle(
kwargs['height'], kwargs['width'], parent=self)
elif 'radius' in kwargs:
self._shape = nngt.geometry.Shape.disk(
kwargs['radius'], parent=self)
elif 'radii' in kwargs:
self._shape = nngt.geometry.Shape.ellipse(
kwargs['radii'], parent=self)
elif 'polygon' in kwargs:
self._shape = nngt.geometry.Shape.from_polygon(
kwargs['polygon'], min_x=kwargs.get('min_x', -5000.),
max_x=kwargs.get('max_x', 5000.),
unit=kwargs.get('unit', 'um'), parent=self)
else:
raise RuntimeError('SpatialGraph needs a `shape` or '
'keywords arguments to build one, or '
'at least `positions` so it can create '
'a square containing them')
else:
minx, maxx = np.min(positions[:, 0]), np.max(positions[:, 0])
miny, maxy = np.min(positions[:, 1]), np.max(positions[:, 1])
height, width = 1.01*(maxy - miny), 1.01*(maxx - minx)
centroid = (0.5*(maxx + minx), 0.5*(maxy + miny))
self._shape = nngt.geometry.Shape.rectangle(
height, width, centroid=centroid, parent=self)
b_rnd_pos = True if not self.node_nb() or positions is None else False
self._pos = self._shape.seed_neurons() if b_rnd_pos else positions
nngt.core.Connections.distances(self)
#-------------------------------------------------------------------------#
# Getters
[docs] def get_positions(self, neurons=None):
'''
Returns the neurons' positions as a (N, 2) array.
Parameters
----------
neurons : int or array-like, optional (default: all neurons)
List of the neurons for which the position should be returned.
'''
if neurons is not None:
return np.array(self._pos[neurons])
return np.array(self._pos)
# ------- #
# Network #
# ------- #
[docs]class Network(Graph):
"""
The detailed class that inherits from :class:`Graph` and implements
additional properties to describe various biological functions
and interact with the NEST simulator.
"""
#-------------------------------------------------------------------------#
# Class attributes and methods
__num_networks = 0
__max_id = 0
[docs] @classmethod
def num_networks(cls):
''' Returns the number of alive instances. '''
return cls.__num_networks
[docs] @classmethod
def from_gids(cls, gids, get_connections=True, get_params=False,
neuron_model=default_neuron, neuron_param=None,
syn_model=default_synapse, syn_param=None, **kwargs):
'''
Generate a network from gids.
Warning
-------
Unless `get_connections` and `get_params` is True, or if your
population is homogeneous and you provide the required information, the
information contained by the network and its `population` attribute
will be erroneous!
To prevent conflicts the :func:`~nngt.Network.to_nest` function is not
available. If you know what you are doing, you should be able to find a
workaround...
Parameters
----------
gids : array-like
Ids of the neurons in NEST or simply user specified ids.
get_params : bool, optional (default: True)
Whether the parameters should be obtained from NEST (can be very
slow).
neuron_model : string, optional (default: None)
Name of the NEST neural model to use when simulating the activity.
neuron_param : dict, optional (default: {})
Dictionary containing the neural parameters; the default value will
make NEST use the default parameters of the model.
syn_model : string, optional (default: 'static_synapse')
NEST synaptic model to use when simulating the activity.
syn_param : dict, optional (default: {})
Dictionary containing the synaptic parameters; the default value
will make NEST use the default parameters of the model.
Returns
-------
net : :class:`~nngt.Network` or subclass
Uniform network of disconnected neurons.
'''
from nngt.lib.errors import not_implemented
if neuron_param is None:
neuron_param = {}
if syn_param is None:
syn_param = {}
# create the population
size = len(gids)
nodes = [i for i in range(size)]
group = nngt.NeuralGroup(
nodes, ntype=1, neuron_model=neuron_model, neuron_param=neuron_param)
pop = nngt.NeuralPop.from_groups([group])
# create the network
net = cls(population=pop, **kwargs)
net.nest_gid = np.array(gids)
net._id_from_nest_gid = {gid: i for i, gid in enumerate(gids)}
net.to_nest = not_implemented
if get_connections:
from nngt.simulation import get_nest_adjacency
converter = {gid: i for i, gid in enumerate(gids)}
mat = get_nest_adjacency(converter)
edges = np.array(mat.nonzero()).T
w = mat.data
net.new_edges(edges, {'weight': w}, check_edges=False)
if get_params:
raise NotImplementedError('`get_params` not implemented yet.')
return net
[docs] @classmethod
@deprecated("1.0", reason="redondant name", alternative="exc_and_inhib")
def ei_network(cls, *args, **kwargs):
return cls.exc_and_inhib(*args, **kwargs)
[docs] @classmethod
def exc_and_inhib(cls, size, iratio=0.2, en_model=default_neuron,
en_param=None, in_model=default_neuron, in_param=None,
syn_spec=None, **kwargs):
'''
Generate a network containing a population of two neural groups:
inhibitory and excitatory neurons.
.. versionadded:: 1.0
.. versionchanged:: 0.8
Removed `es_{model, param}` and `is_{model, param}` in favour of
`syn_spec` parameter.
Renamed `ei_ratio` to `iratio` to match
:func:`~nngt.NeuralPop.exc_and_inhib`.
Parameters
----------
size : int
Number of neurons in the network.
i_ratio : double, optional (default: 0.2)
Ratio of inhibitory neurons: :math:`\\frac{N_i}{N_e+N_i}`.
en_model : string, optional (default: 'aeif_cond_alpha')
Nest model for the excitatory neuron.
en_param : dict, optional (default: {})
Dictionary of parameters for the the excitatory neuron.
in_model : string, optional (default: 'aeif_cond_alpha')
Nest model for the inhibitory neuron.
in_param : dict, optional (default: {})
Dictionary of parameters for the the inhibitory neuron.
syn_spec : dict, optional (default: static synapse)
Dictionary containg a directed edge between groups as key and the
associated synaptic parameters for the post-synaptic neurons (i.e.
those of the second group) as value. If provided, all connections
between groups will be set according to the values contained in
`syn_spec`. Valid keys are:
- `('excitatory', 'excitatory')`
- `('excitatory', 'inhibitory')`
- `('inhibitory', 'excitatory')`
- `('inhibitory', 'inhibitory')`
Returns
-------
net : :class:`~nngt.Network` or subclass
Network of disconnected excitatory and inhibitory neurons.
See also
--------
:func:`~nngt.NeuralPop.exc_and_inhib`
'''
pop = nngt.NeuralPop.exc_and_inhib(
size, iratio, en_model, en_param, in_model, in_param,
syn_spec=syn_spec)
net = cls(population=pop, **kwargs)
return net
#-------------------------------------------------------------------------#
# Constructor, destructor and attributes
def __init__(self, name="Network", weighted=True, directed=True,
from_graph=None, population=None, inh_weight_factor=1.,
**kwargs):
'''
Initializes :class:`~nngt.Network` instance.
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
from_graph : :class:`~nngt.core.GraphObject`, optional (default: None)
An optional :class:`~nngt.core.GraphObject` to serve as base.
population : :class:`nngt.NeuralPop`, (default: None)
An object containing the neural groups and their properties:
model(s) to use in NEST to simulate the neurons as well as their
parameters.
inh_weight_factor : float, optional (default: 1.)
Factor to apply to inhibitory synapses, to compensate for example
the strength difference due to timescales between excitatory and
inhibitory synapses.
Returns
-------
self : :class:`~nggt.Network`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
if population is None:
raise InvalidArgument("Network needs a NeuralPop to be created")
nodes = population.size
if "nodes" in kwargs.keys():
assert kwargs["nodes"] == nodes, "Incompatible values for " +\
"`nodes` = {} with a `population` of size {}.".format(
kwargs["nodes"], nodes)
del kwargs["nodes"]
if "delays" not in kwargs: # set default delay to 1.
kwargs["delays"] = 1.
super(Network, self).__init__(
nodes=nodes, name=name, weighted=weighted, directed=directed,
from_graph=from_graph, inh_weight_factor=inh_weight_factor,
**kwargs)
self._init_bioproperties(population)
if "shape" in kwargs or "positions" in kwargs:
self.make_spatial(self, shape=kwargs.get("shape", None),
positions=kwargs.get("positions", None))
def __del__(self):
super(Network, self).__del__()
self.__class__.__num_networks -= 1
@property
def population(self):
'''
:class:`~nngt.NeuralPop` that divides the neurons into groups with
specific properties.
'''
return self._population
@population.setter
def population(self, population):
if issubclass(population.__class__, nngt.NeuralPop):
if self.node_nb() == population.size:
if population.is_valid:
self._population = population
else:
raise AttributeError("NeuralPop is not valid (not all \
neurons are associated to a group).")
else:
raise AttributeError("{} and NeuralPop must have same number \
of neurons".format(self.__class__.__name__))
else:
raise AttributeError("Expecting NeuralPop but received \
{}".format(pop.__class__.__name__))
@property
def nest_gid(self):
return self._nest_gid
@nest_gid.setter
def nest_gid(self, gids):
self._nest_gid = gids
for group in self.population.values():
group._nest_gids = gids[group.ids]
[docs] def get_edge_types(self):
inhib_neurons = {}
types = np.ones(self.edge_nb())
for g in self._population.values():
if g.neuron_type == -1:
for n in g.ids:
inhib_neurons[n] = None
for i, e in enumerate(self.edges_array):
if e[0] in inhib_neurons:
types[i] = -1
return types
[docs] def id_from_nest_gid(self, gids):
'''
Return the ids of the nodes in the :class:`nngt.Network` instance from
the corresponding NEST gids.
Parameters
----------
gids : int or tuple
NEST gids.
Returns
-------
ids : int or tuple
Ids in the network. Same type as the requested `gids` type.
'''
if nonstring_container(gids):
return np.array([self._id_from_nest_gid[gid] for gid in gids],
dtype=int)
else:
return self._id_from_nest_gid[gids]
[docs] def to_nest(self, send_only=None, use_weights=True):
'''
Send the network to NEST.
.. seealso::
:func:`~nngt.simulation.make_nest_network` for parameters
'''
from nngt.simulation import make_nest_network
if nngt._config['with_nest']:
return make_nest_network(
self, send_only=send_only, use_weights=use_weights)
else:
raise RuntimeError("NEST is not present.")
#-------------------------------------------------------------------------#
# Init tool
def _init_bioproperties(self, population):
''' Set the population attribute and link each neuron to its group. '''
self._population = None
self._nest_gid = None
self._id_from_nest_gid = None
if not hasattr(self, '_iwf'):
self._iwf = 1.
if issubclass(population.__class__, nngt.NeuralPop):
if population.is_valid or not self.node_nb():
self._population = population
nodes = population.size
# create the delay attribute if necessary
if "delay" not in self.edges_attributes:
self.set_delays()
else:
raise AttributeError("NeuralPop is not valid (not all neurons "
"are associated to a group).")
else:
raise AttributeError("Expected NeuralPop but received "
"{}".format(pop.__class__.__name__))
#-------------------------------------------------------------------------#
# Setter
[docs] def set_types(self, syn_type, nodes=None, fraction=None):
raise NotImplementedError("Cannot be used on :class:`~nngt.Network`.")
[docs] def get_neuron_type(self, neuron_ids):
'''
Return the type of the neurons (+1 for excitatory, -1 for inhibitory).
Parameters
----------
neuron_ids : int or tuple
NEST gids.
Returns
-------
ids : int or tuple
Ids in the network. Same type as the requested `gids` type.
'''
if is_integer(neuron_ids):
group_name = self._population._neuron_group[neuron_ids]
ntype = self._population[group_name].neuron_type
return ntype
else:
groups = (self._population._neuron_group[i] for i in neuron_ids)
types = tuple(self._population[gn].neuron_type for gn in groups)
return types
#-------------------------------------------------------------------------#
# Getter
[docs] def neuron_properties(self, idx_neuron):
'''
Properties of a neuron in the graph.
Parameters
----------
idx_neuron : int
Index of a neuron in the graph.
Returns
-------
dict of the neuron's properties.
'''
group_name = self._population._neuron_group[idx_neuron]
return self._population[group_name].properties()
# -------------- #
# SpatialNetwork #
# -------------- #
[docs]class SpatialNetwork(Network, SpatialGraph):
"""
Class that inherits from :class:`~nngt.Network` and :class:`SpatialGraph`
to provide a detailed description of a real neural network in space, i.e.
with positions and biological properties to interact with NEST.
"""
#-------------------------------------------------------------------------#
# Class attributes
__num_networks = 0
__max_id = 0
#-------------------------------------------------------------------------#
# Constructor, destructor, and attributes
def __init__(self, population, name="Graph", weighted=True, directed=True,
shape=None, from_graph=None, positions=None, **kwargs):
'''
Initialize Graph instance
Parameters
----------
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
shape : :class:`~nngt.geometry.Shape`, optional (default: None)
Shape of the neurons' environment (None leads to a square of side
1 cm)
positions : :class:`numpy.array`, optional (default: None)
Positions of the neurons; if not specified and `nodes` != 0, then
neurons will be reparted at random inside the
:class:`~nngt.geometry.Shape` object of the instance.
population : class:`~nngt.NeuralPop`, optional (default: None)
Population from which the network will be built.
Returns
-------
self : :class:`~nngt.SpatialNetwork`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
if population is None:
raise InvalidArgument("Network needs a NeuralPop to be created")
nodes = population.size
super(SpatialNetwork, self).__init__(
nodes=nodes, name=name, weighted=weighted, directed=directed,
shape=shape, positions=positions, population=population,
from_graph=from_graph, **kwargs)
def __del__ (self):
super(SpatialNetwork, self).__del__()
self.__class__.__num_networks -= 1
#-------------------------------------------------------------------------#
# Setter
[docs] def set_types(self, syn_type, nodes=None, fraction=None):
raise NotImplementedError("Cannot be used on "
":class:`~nngt.SpatialNetwork`.")