Skip to content

Networks

Networks: growth, random graph ensembles, robustness, random walks, diffusion, PageRank, structure, percolation.

CopyingVertexElement(label, visual=None, *, age=0.0, degree=0.0, inverse=1.0)

Bases: VertexElement

Node of a vertex-copying network, which holds the inverse of its degree for the copying of its edges.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
age float

The number of steps since the node joined.

0.0
degree float

The number of edges of the node.

0.0
inverse Annotated[float, StateSpec(domain=closed_open(0, oo))]

The inverse of the degree, which only the neighbours of the node read.

1.0

Examples:

>>> from skcomplex.models.mathematics.networks import CopyingVertexElement
>>> CopyingVertexElement('nodes').inverse
1.0
Source code in src/skcomplex/models/mathematics/networks/_copying_vertex.py
40
41
42
43
44
45
46
47
48
49
50
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    age: float = 0.0,
    degree: float = 0.0,
    inverse: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual, age=age, degree=degree)
    self.inverse = inverse

EnsembleNodeElement(label, visual=DEGREE_VISUAL, *, degree=0.0, rate=0.0)

Bases: Element

Node of a random graph, with its degree and the probability that it adds an edge to a random node per step.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

DEGREE_VISUAL
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node, read at the start of every step.

0.0
rate Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that the node adds an edge to a random node in a step.

0.0

Examples:

>>> from skcomplex.models.mathematics.networks import EnsembleNodeElement
>>> EnsembleNodeElement('nodes', rate=0.1).rate
0.1
Source code in src/skcomplex/models/mathematics/networks/_ensemble_node.py
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = DEGREE_VISUAL,
    *,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.degree = degree
    self.rate = rate

FailingElement(label, visual=DEGREE_VISUAL, *, degree=0.0, rate=0.01, targeted=0)

Bases: Element

Node of a network under failures or attacks, with its degree and the parameters of its removal.

The removal of the model reads the parameters: every step a node fails with probability rate under random failures, and under a targeted attack the nodes of the largest degree are removed, one degree class per step.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

DEGREE_VISUAL
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node, read at the start of every step.

0.0
rate Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that a node fails in a step under random failures.

0.01
targeted Annotated[float, ParameterSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 for a targeted attack on the nodes of the largest degree and 0 for random failures.

0

Examples:

>>> from skcomplex.models.mathematics.networks import FailingElement
>>> FailingElement('nodes', targeted=1).targeted
1
Source code in src/skcomplex/models/mathematics/networks/_failing.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = DEGREE_VISUAL,
    *,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.01,
    targeted: Annotated[float, ParameterSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
) -> None:
    super().__init__(label, visual=visual)
    self.degree = degree
    self.rate = rate

    self.targeted = targeted

GiantComponentInteraction(label, *, nodes, links, phase=None)

Bases: Interaction

Activity of every node in its phase, 1 in the giant component of the active links and 0 outside it.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
nodes Annotated[str, RoleSpec()]

The path of the group on the nodes, whose activity the interaction sets.

required
links Annotated[str, RoleSpec()]

The path of the group on the edges, whose activity selects the links of the components.

required
phase str | None

The phase of the step the interaction runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.models.mathematics.networks import GiantComponentInteraction
>>> GiantComponentInteraction('percolation', nodes='nodes', links='links').links
'links'
Source code in src/skcomplex/models/mathematics/networks/_giant_component.py
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    label: str,
    *,
    nodes: Annotated[str, RoleSpec()],
    links: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label)
    self.nodes = nodes
    self.links = links
    self.phase = phase

LinkElement(label, visual=None)

Bases: Element

Edge of a growing network, which holds no state of its own.

Parameters:

Name Type Description Default
label str

The label of the edge group.

required

Examples:

>>> from skcomplex.models.mathematics.networks import LinkElement
>>> LinkElement('links').label
'links'
Source code in src/skcomplex/core/elements/_base.py
70
71
72
def __init__(self, label: str, visual: VisualSpec | None = None) -> None:
    super().__init__(label)
    self.visual = visual

NetworkMeasuresElement(label, visual=None, *, path_length=0.0, assortativity=0.0, modularity=0.0, agreement=0.0, triangle_zscore=0.0)

Bases: Element

Measures of a whole network, held as parameters that the observables of the model report.

Parameters:

Name Type Description Default
label str

The label of the element.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
path_length Annotated[float, ParameterSpec()]

The mean shortest path length of the largest component.

0.0
assortativity Annotated[float, ParameterSpec()]

The degree assortativity.

0.0
modularity Annotated[float, ParameterSpec()]

The modularity of the communities found.

0.0
agreement Annotated[float, ParameterSpec()]

The agreement of the communities found with a reference partition.

0.0
triangle_zscore Annotated[float, ParameterSpec()]

The z-score of the number of triangles against degree-preserving edge switches.

0.0

Examples:

>>> from skcomplex.models.mathematics.networks import NetworkMeasuresElement
>>> NetworkMeasuresElement('measures', modularity=0.4).modularity
0.4
Source code in src/skcomplex/models/mathematics/networks/_network_measures.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    path_length: Annotated[float, ParameterSpec()] = 0.0,
    assortativity: Annotated[float, ParameterSpec()] = 0.0,
    modularity: Annotated[float, ParameterSpec()] = 0.0,
    agreement: Annotated[float, ParameterSpec()] = 0.0,
    triangle_zscore: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.path_length = path_length
    self.assortativity = assortativity
    self.modularity = modularity
    self.agreement = agreement
    self.triangle_zscore = triangle_zscore

NetworkNodeElement(label, visual=VisualSpec('agents', {'color_by': 'community'}), *, degree=0.0, clustering=0.0, betweenness=0.0, community=0.0)

Bases: Element

Node of a network that carries its degree, clustering, betweenness, and community.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'community'})
degree Annotated[float, StateSpec(domain=NN)]

The degree of the node.

0.0
clustering Annotated[float, StateSpec(domain=closed_open(0, oo))]

The clustering coefficient of the node.

0.0
betweenness Annotated[float, StateSpec(domain=closed_open(0, oo))]

The normalized betweenness centrality of the node.

0.0
community Annotated[float, StateSpec(domain=NN)]

The index of the community of the node.

0.0

Examples:

>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.mathematics.networks import NetworkNodeElement
>>> group = GroupElement('nodes', element=NetworkNodeElement, size=3)
>>> group.label, group.element.__name__, group.size
('nodes', 'NetworkNodeElement', 3)
Source code in src/skcomplex/models/mathematics/networks/_network_node.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'community'}),
    *,
    degree: Annotated[float, StateSpec(domain=NN)] = 0.0,
    clustering: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    betweenness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    community: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.degree = degree
    self.clustering = clustering
    self.betweenness = betweenness
    self.community = community

stay(agent, model)

Keep the node and its measures unchanged.

Parameters:

Name Type Description Default
agent AgentView

The node.

required
model ModelView

The model.

required
Source code in src/skcomplex/models/mathematics/networks/_network_node.py
77
78
79
80
81
82
83
84
85
86
def stay(self, agent: AgentView, model: ModelView) -> None:
    """Keep the node and its measures unchanged.

    Args:
        agent:
            The node.

        model:
            The model.
    """

NetworkWalkerElement(label, visual=VisualSpec('agents', {'color_by': 'steps'}), *, steps=0.0)

Bases: Element

Walker on the nodes of a network, which counts the steps it takes.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'steps'})
steps Annotated[float, StateSpec(domain=NN)]

The initial number of steps taken.

0.0

Examples:

>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.mathematics.networks import NetworkWalkerElement
>>> group = GroupElement('walkers', element=NetworkWalkerElement, size=5)
>>> group.label, group.element.__name__, group.size
('walkers', 'NetworkWalkerElement', 5)
Source code in src/skcomplex/models/mathematics/networks/_network_walker.py
36
37
38
39
40
41
42
43
44
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'steps'}),
    *,
    steps: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.steps = steps

NodeContentElement(label, visual=None, *, u=0.0, D=1.0)

Bases: Element

Content of a node of a network, which diffuses along the links with the coefficient D.

Parameters:

Name Type Description Default
label str

The label of the node or the field.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
u Annotated[float, StateSpec()]

The initial content.

0.0
D Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The diffusion coefficient of every link.

1.0

Examples:

>>> from skcomplex.models.mathematics.networks import NodeContentElement
>>> NodeContentElement('node', D=0.5).D
0.5
Source code in src/skcomplex/models/mathematics/networks/_node_content.py
37
38
39
40
41
42
43
44
45
46
47
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    u: Annotated[float, StateSpec()] = 0.0,
    D: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.u = u
    self.D = D

NodeRankElement(label, visual=None, *, rank=0.0, damping=0.85, degree=1.0, n_nodes=1.0)

Bases: Element

Page of a network with its PageRank, its degree, the number of pages, and the damping of the surfer.

Parameters:

Name Type Description Default
label str

The label of the page or the field.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
rank Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial rank.

0.0
damping Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that the surfer follows a link rather than jumping to a random page.

0.85
degree Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of links of the page.

1.0
n_nodes Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of pages of the network.

1.0

Examples:

>>> from skcomplex.models.mathematics.networks import NodeRankElement
>>> NodeRankElement('page', damping=0.9).damping
0.9
Source code in src/skcomplex/models/mathematics/networks/_node_rank.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    rank: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    damping: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.85,
    degree: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    n_nodes: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.rank = rank
    self.damping = damping
    self.degree = degree
    self.n_nodes = n_nodes

RegulatedLinkElement(label, visual=None, *, active=1.0, sign=1)

Bases: Element

Link of a network with triadic regulation, with its activity and the sign of its regulating node.

Parameters:

Name Type Description Default
label str

The label of the link group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
active Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 when the link is active, and 0 otherwise.

1.0
sign Annotated[float, ParameterSpec(domain=FiniteEnumeratedSet([-1, 1]))]

1 when the regulating node lets the link be active while the node is active, and -1 when it lets it be active while the node is inactive.

1

Examples:

>>> from skcomplex.models.mathematics.networks import RegulatedLinkElement
>>> RegulatedLinkElement('links', sign=-1).sign
-1
Source code in src/skcomplex/models/mathematics/networks/_regulated_link.py
37
38
39
40
41
42
43
44
45
46
47
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    active: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    sign: Annotated[float, ParameterSpec(domain=FiniteEnumeratedSet([-1, 1]))] = 1,
) -> None:
    super().__init__(label, visual=visual)
    self.active = active
    self.sign = sign

RegulatedNodeElement(label, visual=ACTIVITY_VISUAL, *, active=1.0)

Bases: Element

Node of a network with triadic regulation, active when it belongs to the giant component of the active links.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

ACTIVITY_VISUAL
active Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 when the node is in the giant component of the active links, and 0 otherwise.

1.0

Examples:

>>> from skcomplex.models.mathematics.networks import RegulatedNodeElement
>>> RegulatedNodeElement('nodes').active
1.0
Source code in src/skcomplex/models/mathematics/networks/_regulated_node.py
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = ACTIVITY_VISUAL,
    *,
    active: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.active = active

SimplexAgeingInteraction(label, *, nodes, triangles)

Bases: HyperedgeInteraction

Ageing of every triangle of a simplicial complex by one per step.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
nodes Annotated[str, RoleSpec()]

The path of the group on the nodes.

required
triangles Annotated[str, RoleSpec()]

The path of the group on the triangles.

required

Examples:

>>> from skcomplex.models.mathematics.networks import SimplexAgeingInteraction
>>> SimplexAgeingInteraction('ageing', nodes='nodes', triangles='triangles').size
3
Source code in src/skcomplex/models/mathematics/networks/_simplex_ageing.py
33
34
35
36
37
38
39
40
41
42
def __init__(
    self,
    label: str,
    *,
    nodes: Annotated[str, RoleSpec()],
    triangles: Annotated[str, RoleSpec()],
) -> None:
    super().__init__(label, size=3)
    self.nodes = nodes
    self.triangles = triangles

SimplexElement(label, visual=None, *, age=0.0)

Bases: Element

Triangle of a growing simplicial complex, with its age in steps.

Parameters:

Name Type Description Default
label str

The label of the triangle or the group on the triangles.

required
visual VisualSpec | None

How the triangle is drawn, or None for default_visual_.

None
age Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of steps since the triangle joined.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the triangle when it sets none.

Examples:

>>> from skcomplex.models.mathematics.networks import SimplexElement
>>> SimplexElement('triangles').age
0.0
Source code in src/skcomplex/models/mathematics/networks/_simplex.py
38
39
40
41
42
43
44
45
46
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    age: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.age = age

SizeBiasedMoveInteraction(label, *, target)

Bases: HyperedgeInteraction

Moves of a random walker across a hyperedge, to every other member with weight |e| - 1.

A walker at node i moves to node j at rate T_ij = k_ij / D_i, where k_ij = sum_e (|e| - 1) over the hyperedges e that hold both, and D_i = sum_j k_ij. On a hyperedge, the focal member gains (|e| - 1) p_j / D_j from every other member j and loses (|e| - 1)^2 p_i / D_i.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of walker densities on the nodes.

required

Examples:

>>> from skcomplex.models.mathematics.networks import SizeBiasedMoveInteraction
>>> SizeBiasedMoveInteraction('moves', target='nodes').target
'nodes'
Source code in src/skcomplex/models/mathematics/networks/_size_biased_move.py
35
36
37
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(label)
    self.target = target

TriadVertexElement(label, visual=None, *, age=0.0, degree=0.0, near=0.0, triad=0.8, extra=1)

Bases: VertexElement

Node of a Holme-Kim network, which closes triangles with the neighbours of its first contact when it joins.

One step after a node joins with one edge by preferential attachment, the triad formation of the model adds extra edges to it. Each new end is a neighbour of its first contact with probability triad, and a node drawn by preferential attachment otherwise. The state near marks the nodes next to the newest node.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
age float

The number of steps since the node joined.

0.0
degree float

The number of edges of the node.

0.0
near Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of neighbours of the node that joined in the previous step.

0.0
triad Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that a new edge closes a triangle.

0.8
extra Annotated[int, ParameterSpec(domain=NN)]

The number of edges a node adds after its first one.

1

Examples:

>>> from skcomplex.models.mathematics.networks import TriadVertexElement
>>> TriadVertexElement('nodes', triad=0.5).triad
0.5
Source code in src/skcomplex/models/mathematics/networks/_triad_vertex.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    age: float = 0.0,
    degree: float = 0.0,
    near: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    triad: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.8,
    extra: Annotated[int, ParameterSpec(domain=NN)] = 1,
) -> None:
    super().__init__(label, visual=visual, age=age, degree=degree)
    self.near = near
    self.triad = triad
    self.extra = extra

TriadicRegulationInteraction(label, *, nodes, links, p=0.5, phase=None)

Bases: HyperedgeInteraction

Regulation of every link by a third node, which lets the link be active with probability p in its phase.

Every hyperedge (i, j, r) holds the two ends of the link (i, j) and its regulating node r. In every step the link becomes active with probability p when a positive regulator is active or a negative regulator is inactive, and inactive otherwise.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
nodes Annotated[str, RoleSpec()]

The path of the group on the nodes, whose activity the regulators read.

required
links Annotated[str, RoleSpec()]

The path of the group on the edges, whose activity the regulation sets.

required
p Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a link the regulation allows is active.

0.5
phase str | None

The phase of the step the regulation runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.models.mathematics.networks import TriadicRegulationInteraction
>>> TriadicRegulationInteraction('regulation', nodes='nodes', links='links').check().size
3
Source code in src/skcomplex/models/mathematics/networks/_triadic_regulation.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    label: str,
    *,
    nodes: Annotated[str, RoleSpec()],
    links: Annotated[str, RoleSpec()],
    p: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.5,
    phase: str | None = None,
) -> None:
    super().__init__(label, size=3)
    self.nodes = nodes
    self.links = links
    self.p = p
    self.phase = phase

VertexElement(label, visual=DEGREE_VISUAL, *, age=0.0, degree=0.0)

Bases: Element

Node of a growing network, with its age in steps and its degree.

New nodes start with age 0, and the maps of the model age the node and read its degree from the edges at the start of every step.

Parameters:

Name Type Description Default
label str

The label of the node group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

DEGREE_VISUAL
age Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of steps since the node joined.

0.0
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node.

0.0

Examples:

>>> from skcomplex.models.mathematics.networks import VertexElement
>>> VertexElement('nodes').age
0.0
Source code in src/skcomplex/models/mathematics/networks/_vertex.py
42
43
44
45
46
47
48
49
50
51
52
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = DEGREE_VISUAL,
    *,
    age: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.age = age
    self.degree = degree

WalkerDensityElement(label, visual=None, *, p=0.0, D=1.0)

Bases: Element

Probability that a random walker is at a node of a hypergraph, with the weight D of the node.

Parameters:

Name Type Description Default
label str

The label of the node or the group on the nodes.

required
visual VisualSpec | None

How the node is drawn, or None for default_visual_.

None
p Annotated[float, StateSpec(domain=closed(0, 1))]

The probability of the walker at the node.

0.0
D Annotated[float, ParameterSpec(domain=open(0, oo))]

The total weight of the moves out of the node, the sum of (|e| - 1)^2 over its hyperedges e.

1.0

Attributes:

Name Type Description
default_visual_

The visual of the node when it sets none.

Examples:

>>> from skcomplex.models.mathematics.networks import WalkerDensityElement
>>> WalkerDensityElement('node', D=4.0).D
4.0
Source code in src/skcomplex/models/mathematics/networks/_walker_density.py
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    p: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
    D: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.p = p
    self.D = D

barabasi_albert(n_nodes=303, *, edges=2, seed_nodes=3)

Return Barabási-Albert growth, whose degrees follow a power law P(k) ~ k^-3 by preferential attachment.

The network starts complete on seed_nodes nodes, and every step one node joins with edges edges to nodes drawn with probability proportional to their degree, until it holds n_nodes nodes.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the network holds room for.

303
edges int

The number of edges of every new node.

2
seed_nodes int

The number of nodes of the complete seed network.

3

Returns:

Name Type Description
system System

The model, with the groups nodes and links and the arrival growth.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.networks import barabasi_albert
>>> trajectory = Simulation(barabasi_albert(13), random_state=0).check().prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).sources.size)
23
Source code in src/skcomplex/models/mathematics/networks/_growth.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@register_model(
    field='mathematics/networks',
    tags=('network', 'growth', 'preferential attachment', 'scale-free'),
    references=('Barabási and Albert 1999', 'Argun et al. 2021', 'Gros 2015', 'Thurner et al. 2018'),
    defaults={'until': 300},
)
def barabasi_albert(n_nodes: int = 303, *, edges: int = 2, seed_nodes: int = 3) -> System:
    """Return Barabási-Albert growth, whose degrees follow a power law `P(k) ~ k^-3` by preferential attachment.

    The network starts complete on `seed_nodes` nodes, and every step one node joins with `edges` edges to nodes
    drawn with probability proportional to their degree, until it holds `n_nodes` nodes.

    Args:
        n_nodes:
            The number of nodes the network holds room for.

        edges:
            The number of edges of every new node.

        seed_nodes:
            The number of nodes of the complete seed network.

    Returns:
        system:
            The model, with the groups `nodes` and `links` and the arrival `growth`.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.mathematics.networks import barabasi_albert
        >>> trajectory = Simulation(barabasi_albert(13), random_state=0).check().prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).sources.size)
        23
    """
    space = NetworkSpace(nx.complete_graph(seed_nodes))
    nodes = _nodes(VertexElement, n_nodes)
    growth = TopologyInteraction('growth', action='add-node', edges=edges, toward=_degree)
    return _grown(nodes, growth, space, edges * n_nodes + seed_nodes**2)

growing_simplicial_complex(n_nodes=303)

Return a simplicial complex that grows by gluing one triangle per step along an edge drawn uniformly.

The complex starts as one triangle, and every step one node joins and forms a triangle with the ends of an edge drawn uniformly, adding two edges, as the network geometry with flavor s = 0 of Bianconi and Rahmede in two dimensions. A node gains triangles in proportion to its degree, so the degrees follow the power law P(k) ~ k^-3 of preferential attachment, and every triangle ages by one per step.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the complex holds room for.

303

Returns:

Name Type Description
system System

The model, with the groups nodes, links, and triangles, and the arrival growth.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.networks import growing_simplicial_complex
>>> simulation = Simulation(growing_simplicial_complex(13), random_state=0).check().prepare()
>>> simulation.run(until=10).trajectory_.network(-1).hyperedges.shape
(11, 3)
Source code in src/skcomplex/models/mathematics/networks/_hypergraphs.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@register_model(
    field='mathematics/networks',
    tags=('simplicial complex', 'hypergraph', 'growth', 'network geometry', 'scale-free'),
    references=('Bianconi and Rahmede 2016', 'Majhi et al. 2022'),
    defaults={'until': 300},
)
def growing_simplicial_complex(n_nodes: int = 303) -> System:
    """Return a simplicial complex that grows by gluing one triangle per step along an edge drawn uniformly.

    The complex starts as one triangle, and every step one node joins and forms a triangle with the ends of an edge
    drawn uniformly, adding two edges, as the network geometry with flavor `s = 0` of Bianconi and Rahmede in two
    dimensions. A node gains triangles in proportion to its degree, so the degrees follow the power law
    `P(k) ~ k^-3` of preferential attachment, and every triangle ages by one per step.

    Args:
        n_nodes:
            The number of nodes the complex holds room for.

    Returns:
        system:
            The model, with the groups `nodes`, `links`, and `triangles`, and the arrival `growth`.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.mathematics.networks import growing_simplicial_complex
        >>> simulation = Simulation(growing_simplicial_complex(13), random_state=0).check().prepare()
        >>> simulation.run(until=10).trajectory_.network(-1).hyperedges.shape
        (11, 3)
    """
    nodes = GroupElement(
        'nodes',
        element=VertexElement,
        on='sites',
        variable=True,
        capacity=n_nodes,
        values={'age': 1.0},
    )
    links = GroupElement('links', element=LinkElement, on='edges', variable=True, capacity=2 * n_nodes)
    triangles = GroupElement('triangles', element=SimplexElement, on='hyperedges', variable=True, capacity=n_nodes)
    growth = TopologyInteraction('growth', action='add-node', hyperedge=3)
    ageing = SimplexAgeingInteraction('triangle_ageing', nodes='nodes', triangles='triangles')
    return System(
        elements=[nodes, links, triangles],
        interactions=[growth, ageing, *_get_bookkeeping()],
        space=NetworkSpace(nx.complete_graph(3), hyperedges=[[0, 1, 2]]),
        time=DiscreteTime(),
    )

holme_kim(n_nodes=304, *, edges=3, triad=0.8, seed_nodes=4)

Return Holme-Kim growth, scale-free like Barabási-Albert growth but with clustering that stays high.

Every step a node joins with one edge by preferential attachment, and the next step it adds edges - 1 edges, each to a neighbour of its first contact with probability triad, closing a triangle, and by preferential attachment otherwise.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the network holds room for.

304
edges int

The number of edges of every new node.

3
triad float

The probability that an added edge closes a triangle.

0.8
seed_nodes int

The number of nodes of the complete seed network.

4

Returns:

Name Type Description
system System

The model, with the groups nodes and links and the arrival growth.

Examples:

>>> from skcomplex.models.mathematics.networks import holme_kim
>>> holme_kim(20).elements[0].element.__name__
'TriadVertexElement'
Source code in src/skcomplex/models/mathematics/networks/_growth.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@register_model(
    field='mathematics/networks',
    tags=('network', 'growth', 'preferential attachment', 'clustering', 'triad formation'),
    references=('Holme and Kim 2002', 'Argun et al. 2021', 'Gros 2015', 'Thurner et al. 2018'),
    defaults={'until': 300},
)
def holme_kim(n_nodes: int = 304, *, edges: int = 3, triad: float = 0.8, seed_nodes: int = 4) -> System:
    """Return Holme-Kim growth, scale-free like Barabási-Albert growth but with clustering that stays high.

    Every step a node joins with one edge by preferential attachment, and the next step it adds `edges - 1` edges,
    each to a neighbour of its first contact with probability `triad`, closing a triangle, and by preferential
    attachment otherwise.

    Args:
        n_nodes:
            The number of nodes the network holds room for.

        edges:
            The number of edges of every new node.

        triad:
            The probability that an added edge closes a triangle.

        seed_nodes:
            The number of nodes of the complete seed network.

    Returns:
        system:
            The model, with the groups `nodes` and `links` and the arrival `growth`.

    Examples:
        >>> from skcomplex.models.mathematics.networks import holme_kim
        >>> holme_kim(20).elements[0].element.__name__
        'TriadVertexElement'
    """
    space = NetworkSpace(nx.complete_graph(seed_nodes))
    nodes = _nodes(TriadVertexElement, n_nodes, triad=triad, extra=edges - 1)
    near = MapInteraction(
        'near',
        target='nodes',
        state='near',
        function=lambda s: neighbors(where(s.target.other.age < JOINING_AGE, 1, 0)),
    )
    closure = TopologyInteraction(
        'closure',
        target='nodes',
        action='add-edge',
        condition=lambda s: s.source.age < JOINING_AGE,
        toward=_get_triad_weight,
        edges=edges - 1,
    )
    growth = TopologyInteraction('growth', action='add-node', toward=_degree)
    return _grown(nodes, growth, space, edges * n_nodes + seed_nodes**2, [near, closure])

hypergraph_random_walk(hypergraph=None, *, source=0, seed=0)

Return the size-biased random walk on a hypergraph, p_j' = sum_i p_i T_ij - p_j, from one node.

A walker at node i moves to node j at rate T_ij = k_ij / sum_l k_il, with k_ij = sum_e (|e| - 1) e_i e_j over the hyperedges e of the incidence matrix, so large hyperedges carry more of the walk, as in Carletti et al. The probability of the walker spreads from the source, keeps its sum of 1, and settles on the stationary distribution p_i = D_i / sum_l D_l, with D_i = sum_e (|e| - 1)^2 e_i the weight of the hyperedges of i.

Parameters:

Name Type Description Default
hypergraph Hypergraph | None

The XGI hypergraph, or None for a random hypergraph of 30 nodes with hyperedges of sizes 2 to 5.

None
source int

The node the walker starts at.

0
seed int

The seed of the random hypergraph.

0

Returns:

Name Type Description
system System

The model, with the probabilities nodes.p and the weights nodes.D.

Examples:

>>> import xgi
>>> from skcomplex.models.mathematics.networks import hypergraph_random_walk
>>> system = hypergraph_random_walk(xgi.Hypergraph([[0, 1, 2], [2, 3]]))
>>> system.elements[0].values['D'].tolist()
[4.0, 4.0, 5.0, 1.0]
Source code in src/skcomplex/models/mathematics/networks/_hypergraphs.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@register_model(
    field='mathematics/networks',
    tags=('hypergraph', 'random walk', 'master equation', 'ode'),
    references=('Carletti et al. 2020', 'Majhi et al. 2022'),
    defaults={'until': 20.0, 'dt': 0.1},
)
def hypergraph_random_walk(
    hypergraph: xgi.Hypergraph | None = None,
    *,
    source: int = 0,
    seed: int = 0,
) -> System:
    """Return the size-biased random walk on a hypergraph, `p_j' = sum_i p_i T_ij - p_j`, from one node.

    A walker at node `i` moves to node `j` at rate `T_ij = k_ij / sum_l k_il`, with `k_ij = sum_e (|e| - 1) e_i e_j`
    over the hyperedges `e` of the incidence matrix, so large hyperedges carry more of the walk, as in Carletti et al.
    The probability of the walker spreads from the source, keeps its sum of 1, and settles on the stationary
    distribution `p_i = D_i / sum_l D_l`, with `D_i = sum_e (|e| - 1)^2 e_i` the weight of the hyperedges of `i`.

    Args:
        hypergraph:
            The XGI hypergraph, or `None` for a random hypergraph of 30 nodes with hyperedges of sizes 2 to 5.

        source:
            The node the walker starts at.

        seed:
            The seed of the random hypergraph.

    Returns:
        system:
            The model, with the probabilities `nodes.p` and the weights `nodes.D`.

    Examples:
        >>> import xgi
        >>> from skcomplex.models.mathematics.networks import hypergraph_random_walk
        >>> system = hypergraph_random_walk(xgi.Hypergraph([[0, 1, 2], [2, 3]]))
        >>> system.elements[0].values['D'].tolist()
        [4.0, 4.0, 5.0, 1.0]
    """
    if hypergraph is None:
        hypergraph = xgi.fast_random_hypergraph(30, [0.1, 0.01, 0.001, 0.0005], seed=seed)
    space = NetworkSpace(nx.empty_graph(sorted(hypergraph.nodes)), hyperedges=hypergraph)
    prepared = space.check().prepare()
    weights = np.zeros(prepared.size_)
    for hyperedge in prepared.hyperedges_:
        weights[list(hyperedge)] += (len(hyperedge) - 1) ** 2
    probabilities = np.zeros(prepared.size_)
    probabilities[source] = 1.0
    nodes = LatticeElement(
        'nodes',
        element=WalkerDensityElement,
        space=space,
        values={'p': probabilities, 'D': np.maximum(weights, 1.0)},
        visual=VisualSpec('sites', {'states': ['p'], 'low': 0.0, 'high': 1.0}),
    )
    return System(elements=[nodes], interactions=[SizeBiasedMoveInteraction('moves', target='nodes')])

network_attack(n_nodes=300, *, edges=2, strategy='targeted', rate=0.01, random_state=0)

Return failures or attacks on a scale-free network, robust to random failures but fragile to attacks on hubs.

The network is a Barabási-Albert graph. Under random failures every node fails with probability rate per step, and the giant component shrinks slowly. Under a targeted attack the nodes of the largest degree are removed every step, and the giant component breaks up after a few percent of the nodes are gone.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

300
edges int

The number of edges every node of the Barabási-Albert graph joins with.

2
strategy str

'random' for random failures or 'targeted' for attacks on the largest degree.

'targeted'
rate float

The probability that a node fails in a step under random failures.

0.01
random_state int | Generator | None

The seed or generator of the network.

0

Returns:

Name Type Description
system System

The model, with the groups nodes and links.

Raises:

Type Description
ValidationError

The strategy is unknown.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.networks import network_attack
>>> trajectory = Simulation(network_attack(50), random_state=0).check().prepare().run(until=3).trajectory_
>>> int(trajectory.network(-1).nodes.size) < 50
True
Source code in src/skcomplex/models/mathematics/networks/_ensembles.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@register_model(
    field='mathematics/networks',
    tags=('network', 'robustness', 'percolation', 'attack', 'scale-free'),
    references=('Albert, Jeong, and Barabási 2000', 'Cohen et al. 2000', 'Ramasco 2020'),
    defaults={'until': 100},
)
def network_attack(
    n_nodes: int = 300,
    *,
    edges: int = 2,
    strategy: str = 'targeted',
    rate: float = 0.01,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return failures or attacks on a scale-free network, robust to random failures but fragile to attacks on hubs.

    The network is a Barabási-Albert graph. Under random failures every node fails with probability `rate` per step,
    and the giant component shrinks slowly. Under a targeted attack the nodes of the largest degree are removed
    every step, and the giant component breaks up after a few percent of the nodes are gone.

    Args:
        n_nodes:
            The number of nodes.

        edges:
            The number of edges every node of the Barabási-Albert graph joins with.

        strategy:
            `'random'` for random failures or `'targeted'` for attacks on the largest degree.

        rate:
            The probability that a node fails in a step under random failures.

        random_state:
            The seed or generator of the network.

    Returns:
        system:
            The model, with the groups `nodes` and `links`.

    Raises:
        ValidationError:
            The strategy is unknown.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.mathematics.networks import network_attack
        >>> trajectory = Simulation(network_attack(50), random_state=0).check().prepare().run(until=3).trajectory_
        >>> int(trajectory.network(-1).nodes.size) < 50
        True
    """
    if strategy not in STRATEGIES:
        error_msg = f'Unknown strategy {strategy!r}. Use one of {", ".join(STRATEGIES)}.'
        raise ValidationError(error_msg)
    space = NetworkSpace(nx.barabasi_albert_graph(n_nodes, edges, seed=random_state))
    targeted = float(strategy == 'targeted')
    groups = _variable_network(FailingElement, space, space.graph.number_of_edges(), rate=rate, targeted=targeted)
    removal = TopologyInteraction('removal', target='nodes', action='remove-node', probability=_get_removal)
    return System(elements=groups, interactions=[_get_degree(), removal], space=space, time=DiscreteTime())

network_diffusion(graph=None, *, D=1.0, source=0)

Return diffusion on a network, du_i/dt = D Σ_j A_ij (u_j - u_i), which spreads a unit mass to every node.

The total is conserved, and on a connected graph the content of every node tends to 1 / n.

Parameters:

Name Type Description Default
graph Graph | None

The undirected NetworkX graph, or None for Zachary's karate club.

None
D float

The diffusion coefficient of every link.

1.0
source int

The index, in node order, of the node that holds the whole mass at the start.

0

Returns:

Name Type Description
system System

The model, with the contents nodes.u.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.mathematics.networks import network_diffusion
>>> network_diffusion(nx.path_graph(4)).elements[0].check().prepare().size_
4
Source code in src/skcomplex/models/mathematics/networks/_processes.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@register_model(
    field='mathematics/networks',
    tags=('network', 'diffusion', 'graph laplacian', 'ode'),
    references=('Thurner et al. 2018', 'Jensen 2023'),
    defaults={'until': 10.0, 'dt': 0.1},
)
def network_diffusion(graph: nx.Graph | None = None, *, D: float = 1.0, source: int = 0) -> System:
    """Return diffusion on a network, `du_i/dt = D Σ_j A_ij (u_j - u_i)`, which spreads a unit mass to every node.

    The total is conserved, and on a connected graph the content of every node tends to `1 / n`.

    Args:
        graph:
            The undirected NetworkX graph, or `None` for Zachary's karate club.

        D:
            The diffusion coefficient of every link.

        source:
            The index, in node order, of the node that holds the whole mass at the start.

    Returns:
        system:
            The model, with the contents `nodes.u`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.mathematics.networks import network_diffusion
        >>> network_diffusion(nx.path_graph(4)).elements[0].check().prepare().size_
        4
    """
    graph = _graph_or_default(graph)
    initial = np.zeros(graph.number_of_nodes())
    initial[source] = 1.0
    nodes = FieldElement('nodes', element=NodeContentElement, space=NetworkSpace(graph), values={'u': initial, 'D': D})
    diffusion = DiffusionInteraction('diffusion', target='nodes', state='u', coefficient=lambda s: s.target.D)
    return System(elements=[nodes], interactions=[diffusion])

network_random_walk(graph=None, *, n_walkers=50)

Return random walkers on a network, which visit every node of a connected graph in proportion to its degree.

Parameters:

Name Type Description Default
graph Graph | None

The undirected NetworkX graph, or None for Zachary's karate club.

None
n_walkers int

The number of walkers, started at random nodes.

50

Returns:

Name Type Description
system System

The model, with the group walkers.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.mathematics.networks import network_random_walk
>>> network_random_walk(nx.cycle_graph(6), n_walkers=3).elements[0].check().prepare().size_
3
Source code in src/skcomplex/models/mathematics/networks/_processes.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@register_model(
    field='mathematics/networks',
    tags=('network', 'random walk', 'agents'),
    references=('Thurner et al. 2018', 'Jensen 2023'),
    defaults={'until': 100},
)
def network_random_walk(graph: nx.Graph | None = None, *, n_walkers: int = 50) -> System:
    """Return random walkers on a network, which visit every node of a connected graph in proportion to its degree.

    Args:
        graph:
            The undirected NetworkX graph, or `None` for Zachary's karate club.

        n_walkers:
            The number of walkers, started at random nodes.

    Returns:
        system:
            The model, with the group `walkers`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.mathematics.networks import network_random_walk
        >>> network_random_walk(nx.cycle_graph(6), n_walkers=3).elements[0].check().prepare().size_
        3
    """
    graph = _graph_or_default(graph)
    walk = MovementInteraction('walk', target='walkers', step='neighbor', cost=_count_step)
    return System(
        elements=[GroupElement('walkers', element=NetworkWalkerElement, size=n_walkers)],
        interactions=[walk],
        space=NetworkSpace(graph),
    )

network_structure_analysis(graph=None, *, reference='block', null_samples=10, random_state=0)

Return a network recorded with its centralities, degree mixing, communities, and triangle z-score.

Every node is an agent that carries its degree, clustering, betweenness, and the community found by Louvain modularity maximization. The observables report the clustering, the mean path length of the largest component, the degree assortativity, the modularity, the agreement of the communities with a reference partition, and the z-score of triangles against degree-preserving edge switches.

Parameters:

Name Type Description Default
graph Graph | None

The undirected NetworkX graph, or None for a stochastic block model of 4 blocks of 100 nodes with mean in-block degree 16 and out-block degree 2.

None
reference str | None

The node attribute that holds the reference partition, such as 'block' of a stochastic block model or 'club' of Zachary's karate club, or None. The agreement is reported only when every node has it.

'block'
null_samples int

The number of edge-switched graphs of the triangle z-score, which is reported only when positive.

10
random_state int | None

The seed of the block model, the community detection, and the null graphs.

0

Returns:

Name Type Description
system System

The model, with the node group nodes and the network measures measures.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.mathematics.networks import network_structure_analysis
>>> system = network_structure_analysis(nx.karate_club_graph(), reference='club', null_samples=0)
>>> system.elements[0].check().prepare().size_
34
Source code in src/skcomplex/models/mathematics/networks/_structure.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
@register_model(
    field='mathematics/networks',
    tags=('network', 'centrality', 'communities', 'motifs', 'assortativity'),
    references=('Newman 2010', 'Milo et al. 2002', 'Blondel et al. 2008', 'Bertin 2021'),
    defaults={'until': 1, 'dt': 1},
    observables=_structure_observables,
)
def network_structure_analysis(
    graph: nx.Graph | None = None,
    *,
    reference: str | None = 'block',
    null_samples: int = 10,
    random_state: int | None = 0,
) -> System:
    """Return a network recorded with its centralities, degree mixing, communities, and triangle z-score.

    Every node is an agent that carries its degree, clustering, betweenness, and the community found by Louvain
    modularity maximization. The observables report the clustering, the mean path length of the largest component, the
    degree assortativity, the modularity, the agreement of the communities with a reference partition, and the z-score
    of triangles against degree-preserving edge switches.

    Args:
        graph:
            The undirected NetworkX graph, or `None` for a stochastic block model of 4 blocks of 100 nodes with mean
            in-block degree 16 and out-block degree 2.

        reference:
            The node attribute that holds the reference partition, such as `'block'` of a stochastic block model or
            `'club'` of Zachary's karate club, or `None`. The agreement is reported only when every node has it.

        null_samples:
            The number of edge-switched graphs of the triangle z-score, which is reported only when positive.

        random_state:
            The seed of the block model, the community detection, and the null graphs.

    Returns:
        system:
            The model, with the node group `nodes` and the network measures `measures`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.mathematics.networks import network_structure_analysis
        >>> system = network_structure_analysis(nx.karate_club_graph(), reference='club', null_samples=0)
        >>> system.elements[0].check().prepare().size_
        34
    """
    network = _planted_partition(random_state) if graph is None else graph
    found = (
        NetworkAnalysis('communities', reference=_reference(network, reference), random_state=random_state)
        .check()
        .prepare(network)
        .run()
    )
    summary = NetworkAnalysis('summary').check().prepare(network).run()
    clustering = nx.clustering(network)
    betweenness = NetworkAnalysis('centrality', measure='betweenness').check().prepare(network).run().centrality_[0]
    nodes = list(network.nodes())
    group = GroupElement(
        'nodes',
        element=NetworkNodeElement,
        size=len(nodes),
        values={
            'degree': np.asarray([network.degree(node) for node in nodes], dtype=float),
            'clustering': np.asarray([clustering[node] for node in nodes], dtype=float),
            'betweenness': np.asarray([betweenness[node] for node in nodes], dtype=float),
            'community': found.labels_[0].astype(float),
        },
        location=nodes,
    )
    zscore = 0.0
    if null_samples:
        motifs = MotifAnalysis('z-scores', samples=null_samples, random_state=random_state)
        zscore = motifs.check().prepare(network).run().z_scores_[0]['triangle']
    measures = NetworkMeasuresElement(
        'measures',
        path_length=float(summary.path_length_[0]),
        assortativity=float(NetworkAnalysis('assortativity').check().prepare(network).run().assortativity_[0]),
        modularity=float(found.modularity_[0]),
        agreement=0.0 if found.agreement_ is None else float(found.agreement_[0]),
        triangle_zscore=zscore,
    )
    return System(elements=[group, measures], space=NetworkSpace(network))

pagerank(graph=None, *, damping=0.85)

Return the PageRank iteration r_i' = (1 - d) / n + d Σ_j A_ij r_j / k_j on an undirected network.

The ranks start uniform, stay normalized, and converge to the PageRank of the graph. Every node needs a link.

Parameters:

Name Type Description Default
graph Graph | None

The undirected NetworkX graph without isolated nodes, or None for Zachary's karate club.

None
damping float

The probability d that the surfer follows a link rather than jumping to a random node.

0.85

Returns:

Name Type Description
system System

The model, with the ranks pages.rank.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.mathematics.networks import pagerank
>>> pagerank(nx.cycle_graph(5)).elements[0].check().prepare().size_
5
Source code in src/skcomplex/models/mathematics/networks/_processes.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@register_model(
    field='mathematics/networks',
    tags=('network', 'pagerank', 'centrality', 'discrete map'),
    references=('Brin and Page 1998', 'Thurner et al. 2018', 'Jensen 2023'),
    defaults={'until': 50, 'dt': 1},
)
def pagerank(graph: nx.Graph | None = None, *, damping: float = 0.85) -> System:
    """Return the PageRank iteration `r_i' = (1 - d) / n + d Σ_j A_ij r_j / k_j` on an undirected network.

    The ranks start uniform, stay normalized, and converge to the PageRank of the graph. Every node needs a link.

    Args:
        graph:
            The undirected NetworkX graph without isolated nodes, or `None` for Zachary's karate club.

        damping:
            The probability `d` that the surfer follows a link rather than jumping to a random node.

    Returns:
        system:
            The model, with the ranks `pages.rank`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.mathematics.networks import pagerank
        >>> pagerank(nx.cycle_graph(5)).elements[0].check().prepare().size_
        5
    """
    graph = _graph_or_default(graph)
    size = graph.number_of_nodes()
    degrees = np.array([degree for _, degree in graph.degree()], dtype=float)
    values = {'rank': np.full(size, 1 / size), 'degree': degrees, 'n_nodes': float(size), 'damping': damping}
    pages = FieldElement('pages', element=NodeRankElement, space=NetworkSpace(graph), values=values)
    links = CouplingInteraction('links', target='pages', read='rank', write='rank', function=_get_link_term, form='map')
    return System(elements=[pages], interactions=[links])

random_graph_ensembles(n_nodes=200, *, ensemble='erdos_renyi', mean_degree=4.0, rewiring=0.2, steps=100)

Return the construction of an Erdős-Rényi or a Watts-Strogatz random graph, one step of new edges at a time.

The Erdős-Rényi graph starts empty and every node adds an edge to a random node with a probability that brings the mean degree to mean_degree after steps steps, so a giant component emerges when the mean degree passes 1. The Watts-Strogatz graph starts as a ring where every node joins its mean_degree nearest nodes, and every edge moves its target to a random node at a rate that rewires a fraction rewiring of the edges after steps steps, which shortens paths while the clustering stays high.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

200
ensemble str

'erdos_renyi' or 'watts_strogatz'.

'erdos_renyi'
mean_degree float

The mean degree of the final graph, an even number for the Watts-Strogatz ring.

4.0
rewiring float

The fraction of the edges of the Watts-Strogatz ring rewired after steps steps.

0.2
steps int

The number of steps of the construction.

100

Returns:

Name Type Description
system System

The model, with the groups nodes and links, the map degree, and the interaction linking or rewiring.

Raises:

Type Description
ValidationError

The ensemble is unknown.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.networks import random_graph_ensembles
>>> system = random_graph_ensembles(40, ensemble='watts_strogatz', steps=5)
>>> trajectory = Simulation(system, random_state=0).check().prepare().run(until=5).trajectory_
>>> int(trajectory.network(-1).sources.size)
80
Source code in src/skcomplex/models/mathematics/networks/_ensembles.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register_model(
    field='mathematics/networks',
    tags=('network', 'random graph', 'small world', 'giant component'),
    references=('Erdős and Rényi 1959', 'Watts and Strogatz 1998', 'Barabási 2016'),
    defaults={'until': 100},
)
def random_graph_ensembles(
    n_nodes: int = 200,
    *,
    ensemble: str = 'erdos_renyi',
    mean_degree: float = 4.0,
    rewiring: float = 0.2,
    steps: int = 100,
) -> System:
    """Return the construction of an Erdős-Rényi or a Watts-Strogatz random graph, one step of new edges at a time.

    The Erdős-Rényi graph starts empty and every node adds an edge to a random node with a probability that brings
    the mean degree to `mean_degree` after `steps` steps, so a giant component emerges when the mean degree passes 1.
    The Watts-Strogatz graph starts as a ring where every node joins its `mean_degree` nearest nodes, and every edge
    moves its target to a random node at a rate that rewires a fraction `rewiring` of the edges after `steps` steps,
    which shortens paths while the clustering stays high.

    Args:
        n_nodes:
            The number of nodes.

        ensemble:
            `'erdos_renyi'` or `'watts_strogatz'`.

        mean_degree:
            The mean degree of the final graph, an even number for the Watts-Strogatz ring.

        rewiring:
            The fraction of the edges of the Watts-Strogatz ring rewired after `steps` steps.

        steps:
            The number of steps of the construction.

    Returns:
        system:
            The model, with the groups `nodes` and `links`, the map `degree`, and the interaction `linking` or
            `rewiring`.

    Raises:
        ValidationError:
            The ensemble is unknown.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.mathematics.networks import random_graph_ensembles
        >>> system = random_graph_ensembles(40, ensemble='watts_strogatz', steps=5)
        >>> trajectory = Simulation(system, random_state=0).check().prepare().run(until=5).trajectory_
        >>> int(trajectory.network(-1).sources.size)
        80
    """
    if ensemble not in ENSEMBLES:
        error_msg = f'Unknown ensemble {ensemble!r}. Use one of {", ".join(ENSEMBLES)}.'
        raise ValidationError(error_msg)
    if ensemble == 'erdos_renyi':
        space = NetworkSpace(nx.empty_graph(n_nodes))
        rate = mean_degree / 2 / steps
        groups = _variable_network(
            EnsembleNodeElement,
            space,
            int(np.ceil(1.5 * mean_degree * n_nodes / 2)) + 10,
            rate=rate,
        )
        linking = TopologyInteraction('linking', target='nodes', action='add-edge', probability=lambda s: s.source.rate)
        return System(elements=groups, interactions=[_get_degree(), linking], space=space, time=DiscreteTime())
    space = NetworkSpace(nx.watts_strogatz_graph(n_nodes, int(mean_degree), 0.0))
    rate = 1 - (1 - rewiring) ** (1 / steps)
    groups = _variable_network(EnsembleNodeElement, space, space.graph.number_of_edges())
    rewiring = TopologyInteraction('rewiring', target='nodes', action='rewire', probability=rate)
    return System(elements=groups, interactions=[_get_degree(), rewiring], space=space, time=DiscreteTime())

triadic_percolation(n_nodes=1000, *, mean_degree=4.0, p=0.5, negative=1.0, random_state=0)

Return triadic percolation, where regulating nodes switch links on and off from the giant component.

Every link of an Erdős-Rényi graph has one regulating node, drawn at random among the other nodes, which is negative with probability negative and positive otherwise. The link and its regulator form a hyperedge of size three. Every step is a time step of Millán et al. in two phases. In the phase regulation every link is active with probability p when its positive regulator is active or its negative regulator is inactive, and inactive otherwise. In the phase percolation a node is active when it belongs to the giant component of the links the regulation of the same step left active. All nodes and links start active.

With the fraction R of nodes in the giant component, the links are active with probability p_L = p ((1 - negative) R + negative (1 - R)), and R follows the map R' = S(c p_L), where S(m) solves S = 1 - exp(-m S) for the mean degree c. With negative regulators alone the map decreases, so above the percolation point p = 1 / c its fixed point is unstable and R alternates between a large and a small giant component, a cycle of period two. Below it the giant component vanishes.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

1000
mean_degree float

The mean degree of the Erdős-Rényi graph.

4.0
p float

The probability that a link the regulation allows is active.

0.5
negative float

The probability that the regulator of a link is negative.

1.0
random_state int | Generator | None

The seed or generator of the graph, the regulators, and their signs.

0

Returns:

Name Type Description
system System

The model, with the activities nodes.active and links.active and the signs links.sign.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.networks import triadic_percolation
>>> system = triadic_percolation(200, p=0.8)
>>> trajectory = Simulation(system, random_state=0).check().prepare().run(until=3).trajectory_
>>> giant = trajectory.values('nodes.active').mean(axis=1)
>>> bool(giant[1] < 0.5 < giant[2])
True
Source code in src/skcomplex/models/mathematics/networks/_triadic.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@register_model(
    field='mathematics/networks',
    tags=('network', 'hypergraph', 'percolation', 'triadic interactions', 'period doubling'),
    references=('Sun et al. 2023', 'Millán, Sun, Torres, and Bianconi 2023'),
    defaults={'until': 30, 'dt': 1},
    observables=_giant_observables,
)
def triadic_percolation(
    n_nodes: int = 1000,
    *,
    mean_degree: float = 4.0,
    p: float = 0.5,
    negative: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return triadic percolation, where regulating nodes switch links on and off from the giant component.

    Every link of an Erdős-Rényi graph has one regulating node, drawn at random among the other nodes, which is
    negative with probability `negative` and positive otherwise. The link and its regulator form a hyperedge of size
    three. Every step is a time step of Millán et al. in two phases. In the phase `regulation` every link is active
    with probability `p` when its positive regulator is active or its negative regulator is inactive, and inactive
    otherwise. In the phase `percolation` a node is active when it belongs to the giant component of the links the
    regulation of the same step left active. All nodes and links start active.

    With the fraction `R` of nodes in the giant component, the links are active with probability
    `p_L = p ((1 - negative) R + negative (1 - R))`, and `R` follows the map `R' = S(c p_L)`, where `S(m)` solves
    `S = 1 - exp(-m S)` for the mean degree `c`. With negative regulators alone the map decreases, so above the
    percolation point `p = 1 / c` its fixed point is unstable and `R` alternates between a large and a small giant
    component, a cycle of period two. Below it the giant component vanishes.

    Args:
        n_nodes:
            The number of nodes.

        mean_degree:
            The mean degree of the Erdős-Rényi graph.

        p:
            The probability that a link the regulation allows is active.

        negative:
            The probability that the regulator of a link is negative.

        random_state:
            The seed or generator of the graph, the regulators, and their signs.

    Returns:
        system:
            The model, with the activities `nodes.active` and `links.active` and the signs `links.sign`.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.mathematics.networks import triadic_percolation
        >>> system = triadic_percolation(200, p=0.8)
        >>> trajectory = Simulation(system, random_state=0).check().prepare().run(until=3).trajectory_
        >>> giant = trajectory.values('nodes.active').mean(axis=1)
        >>> bool(giant[1] < 0.5 < giant[2])
        True
    """
    rng = np.random.default_rng(random_state)
    graph = nx.gnp_random_graph(n_nodes, mean_degree / (n_nodes - 1), seed=rng)
    hyperedges, signs = _regulated_links(graph, negative, rng)
    space = NetworkSpace(graph, hyperedges=hyperedges)
    nodes = LatticeElement('nodes', element=RegulatedNodeElement, space=space)
    links = GroupElement('links', element=RegulatedLinkElement, space=space, on='edges', values={'sign': signs})
    interactions = [
        TriadicRegulationInteraction('regulation', nodes='nodes', links='links', p=p, phase='regulation'),
        GiantComponentInteraction('percolation', nodes='nodes', links='links', phase='percolation'),
    ]
    time = DiscreteTime(schedule=PhasesScheduleSpec((PhaseSpec('regulation'), PhaseSpec('percolation'))))
    return System(elements=[nodes, links], interactions=interactions, time=time)

vertex_copying(n_nodes=304, *, edges=3, randomness=0.2, seed_nodes=4)

Return vertex-copying growth, whose new nodes copy the edges of existing nodes and make a heavy-tailed degree.

Every step a node joins with edges edges, each to a random node with probability randomness and otherwise to a random neighbour of a random prototype, which favours nodes of high degree without reading degrees.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the network holds room for.

304
edges int

The number of edges of every new node.

3
randomness float

The probability that an edge joins a random node instead of copying.

0.2
seed_nodes int

The number of nodes of the complete seed network.

4

Returns:

Name Type Description
system System

The model, with the groups nodes and links and the arrival growth.

Examples:

>>> from skcomplex.models.mathematics.networks import vertex_copying
>>> vertex_copying(20).interactions[0].edges
3
Source code in src/skcomplex/models/mathematics/networks/_growth.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@register_model(
    field='mathematics/networks',
    tags=('network', 'growth', 'copying', 'scale-free'),
    references=('Kleinberg et al. 1999', 'Krapivsky and Redner 2005', 'Ramasco 2020'),
    defaults={'until': 300},
)
def vertex_copying(n_nodes: int = 304, *, edges: int = 3, randomness: float = 0.2, seed_nodes: int = 4) -> System:
    """Return vertex-copying growth, whose new nodes copy the edges of existing nodes and make a heavy-tailed degree.

    Every step a node joins with `edges` edges, each to a random node with probability `randomness` and otherwise to
    a random neighbour of a random prototype, which favours nodes of high degree without reading degrees.

    Args:
        n_nodes:
            The number of nodes the network holds room for.

        edges:
            The number of edges of every new node.

        randomness:
            The probability that an edge joins a random node instead of copying.

        seed_nodes:
            The number of nodes of the complete seed network.

    Returns:
        system:
            The model, with the groups `nodes` and `links` and the arrival `growth`.

    Examples:
        >>> from skcomplex.models.mathematics.networks import vertex_copying
        >>> vertex_copying(20).interactions[0].edges
        3
    """
    space = NetworkSpace(nx.complete_graph(seed_nodes))
    nodes = _nodes(CopyingVertexElement, n_nodes)
    growth = TopologyInteraction(
        'growth',
        action='add-node',
        edges=edges,
        toward=lambda node: randomness + (1 - randomness) * neighbors(node.other.inverse),
    )
    inverse = MapInteraction(
        'inverse',
        target='nodes',
        state='inverse',
        function=lambda s: 1 / (neighbors(1) + SMALLEST_DEGREE),
    )
    return _grown(nodes, growth, space, edges * n_nodes + seed_nodes**2, [inverse])