Skip to content

Networks

Networks: growth, turnover, ensembles, robustness, cascades, walks, diffusion, PageRank, structure, percolation.

ClosingNodeElement(label, visual=DEGREE_VISUAL, *, degree=0.0)

Bases: Element

Node of the Davidsen-Ebel-Bornholdt network, with its degree.

A map of the system reads the 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
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node.

0.0

Examples:

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

ClusterNodeElement(label, visual=SIZE_VISUAL, *, size=1.0)

Bases: Element

Node of a network with the number of nodes of its connected component.

A map of the system reads the size from the edges at the start of every step, so it holds the size of the component in the network the requests of the step before left.

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_.

SIZE_VISUAL
size Annotated[float, StateSpec(domain=closed_open(1, oo))]

The number of nodes of the connected component of the node.

1.0

Examples:

>>> from skcomplex.systems.mathematics.networks import ClusterNodeElement
>>> ClusterNodeElement('nodes').size
1.0
Source code in src/skcomplex/systems/mathematics/networks/_explosive.py
46
47
48
49
50
51
52
53
54
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = SIZE_VISUAL,
    *,
    size: Annotated[float, StateSpec(domain=RealSet.closed_open(1, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.size = size

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.systems.mathematics.networks import CopyingVertexElement
>>> CopyingVertexElement('nodes').inverse
1.0
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
51
52
53
54
55
56
57
58
59
60
61
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

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

Bases: Element

Node of two interdependent networks, functional while it belongs to the giant component of both.

The node stands for a pair of nodes, one of each network, that depend on each other, so it fails when either of them leaves the giant component of the functional nodes of its network.

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

Whether the node is functional, 1 or 0.

1.0

Examples:

>>> from skcomplex.systems.mathematics.networks import DependentNodeElement
>>> DependentNodeElement('nodes', active=0.0).active
0.0
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
52
53
54
55
56
57
58
59
60
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

DuplicatingNodeElement(label, visual=DEGREE_VISUAL, *, degree=0.0)

Bases: Element

Node of the duplication-deletion network, with its degree.

A map of the system reads the 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
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node.

0.0

Examples:

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

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.systems.mathematics.networks import EnsembleNodeElement
>>> EnsembleNodeElement('nodes', rate=0.1).rate
0.1
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
85
86
87
88
89
90
91
92
93
94
95
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 system 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.systems.mathematics.networks import FailingElement
>>> FailingElement('nodes', targeted=1).targeted
1
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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

FitnessVertexElement(label, visual=None, *, age=0.0, degree=0.0, fitness=1.0)

Bases: VertexElement

Node of a growing network with an intrinsic fitness, which scales its chance to win new edges.

A new edge joins the node with probability proportional to its fitness times its degree, as in the model of Bianconi and Barabási. The fitness is drawn when the node joins and stays fixed.

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
fitness Annotated[float, StateSpec(domain=closed_open(0, oo))]

The fitness of the node.

1.0

Examples:

>>> from skcomplex.systems.mathematics.networks import FitnessVertexElement
>>> FitnessVertexElement('nodes', fitness=0.5).fitness
0.5
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    age: float = 0.0,
    degree: float = 0.0,
    fitness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual, age=age, degree=degree)
    self.fitness = fitness

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.systems.mathematics.networks import GiantComponentInteraction
>>> GiantComponentInteraction('percolation', nodes='nodes', links='links').links
'links'
Source code in src/skcomplex/systems/mathematics/networks/_triadic.py
83
84
85
86
87
88
89
90
91
92
93
94
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

LoadedNodeElement(label, visual=None, *, load=0.0, capacity=0.0, giant=0.0)

Bases: Element

Node of a network that carries a load, the shortest paths through it, and fails above its capacity.

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
load Annotated[float, StateSpec(domain=closed_open(0, oo))]

The betweenness of the node on the network present, read at the start of every step.

0.0
capacity Annotated[float, StateSpec()]

The largest load the node carries, a negative value for a node that fails at the first step.

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

The number of nodes of the largest connected component, the same for every node.

0.0

Examples:

>>> from skcomplex.systems.mathematics.networks import LoadedNodeElement
>>> LoadedNodeElement('nodes', capacity=12.0).capacity
12.0
Source code in src/skcomplex/systems/mathematics/networks/_overload.py
49
50
51
52
53
54
55
56
57
58
59
60
61
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    load: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    capacity: Annotated[float, StateSpec()] = 0.0,
    giant: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.load = load
    self.capacity = capacity
    self.giant = giant

MergingNodeElement(label, visual=DEGREE_VISUAL, *, degree=0.0)

Bases: Element

Node of the node-merging network, with its degree.

A map of the system reads the 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
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of edges of the node.

0.0

Examples:

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

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 system 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.systems.mathematics.networks import NetworkMeasuresElement
>>> NetworkMeasuresElement('measures', modularity=0.4).modularity
0.4
Source code in src/skcomplex/systems/mathematics/networks/_structure.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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.components.elements import GroupElement
>>> from skcomplex.systems.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/systems/mathematics/common/_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, system)

Keep the node and its measures unchanged.

Parameters:

Name Type Description Default
agent AgentView

The node.

required
system SystemView

The system.

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

    Args:
        agent:
            The node.

        system:
            The system.
    """

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.components.elements import GroupElement
>>> from skcomplex.systems.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/systems/mathematics/networks/_processes.py
131
132
133
134
135
136
137
138
139
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.systems.mathematics.networks import NodeContentElement
>>> NodeContentElement('node', D=0.5).D
0.5
Source code in src/skcomplex/systems/mathematics/networks/_processes.py
46
47
48
49
50
51
52
53
54
55
56
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.systems.mathematics.networks import NodeRankElement
>>> NodeRankElement('page', damping=0.9).damping
0.9
Source code in src/skcomplex/systems/mathematics/networks/_processes.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
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

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

Bases: Element

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

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.systems.mathematics.networks import PercolatingNodeElement
>>> PercolatingNodeElement('nodes').active
1.0
Source code in src/skcomplex/systems/mathematics/networks/_spatial_triadic.py
47
48
49
50
51
52
53
54
55
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

PercolationInteraction(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.systems.mathematics.networks import PercolationInteraction
>>> PercolationInteraction('percolation', nodes='nodes', links='links').links
'links'
Source code in src/skcomplex/systems/mathematics/networks/_spatial_triadic.py
181
182
183
184
185
186
187
188
189
190
191
192
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

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.systems.mathematics.networks import RegulatedLinkElement
>>> RegulatedLinkElement('links', sign=-1).sign
-1
Source code in src/skcomplex/systems/mathematics/networks/_triadic.py
48
49
50
51
52
53
54
55
56
57
58
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.systems.mathematics.networks import RegulatedNodeElement
>>> RegulatedNodeElement('nodes').active
1.0
Source code in src/skcomplex/systems/mathematics/common/_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

SignedLinkElement(label, visual=None, *, active=1.0, negatives=0, positives=1)

Bases: Element

Link regulated by signed nodes, with its activity and the numbers of its negative and positive regulators.

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
negatives Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The number of regulating nodes that let the link be active only while they are inactive, the first regulators of its hyperedge.

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

The number of regulating nodes that let the link be active while they are active, the regulators of its hyperedge after the negative ones.

1

Examples:

>>> from skcomplex.systems.mathematics.networks import SignedLinkElement
>>> SignedLinkElement('links', negatives=2).negatives
2
Source code in src/skcomplex/systems/mathematics/networks/_spatial_triadic.py
85
86
87
88
89
90
91
92
93
94
95
96
97
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    active: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    negatives: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0,
    positives: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1,
) -> None:
    super().__init__(label, visual=visual)
    self.active = active
    self.negatives = negatives
    self.positives = positives

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

Bases: HyperedgeInteraction

Regulation of every link by signed regulating nodes, which let it be active with probability p in its phase.

Every hyperedge (i, j, r_1, ..., r_m) holds the two ends of the link (i, j) and its regulating nodes, first its negatives negative regulators and then its positives positive ones, which the link reads. Further members pad the hyperedges to one size and regulate nothing, so links with several numbers of regulators share one size and one law writes every link once. In every step the link becomes active with probability p when none of its negative regulators is active and at least one of its positive regulators is active, or, for a link without positive regulators, when none of its negative regulators is active, 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.systems.mathematics.networks import SignedRegulationInteraction
>>> SignedRegulationInteraction('regulation', nodes='nodes', links='links').p
0.5
Source code in src/skcomplex/systems/mathematics/networks/_spatial_triadic.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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)
    self.nodes = nodes
    self.links = links
    self.p = p
    self.phase = phase

SimilarityVertexElement(label, visual=None, *, age=0.0, degree=0.0, angle=0.0)

Bases: VertexElement

Node of a popularity-similarity network, with an angle on the circle next to its age and degree.

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
angle Annotated[float, StateSpec()]

The angle of the node on the circle, its similarity coordinate.

0.0

Examples:

>>> from skcomplex.systems.mathematics.networks import SimilarityVertexElement
>>> SimilarityVertexElement('nodes', angle=1.5).angle
1.5
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
130
131
132
133
134
135
136
137
138
139
140
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    age: float = 0.0,
    degree: float = 0.0,
    angle: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual, age=age, degree=degree)
    self.angle = angle

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.systems.mathematics.networks import SimplexAgeingInteraction
>>> SimplexAgeingInteraction('ageing', nodes='nodes', triangles='triangles').size
3
Source code in src/skcomplex/systems/mathematics/networks/_hypergraphs.py
118
119
120
121
122
123
124
125
126
127
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.systems.mathematics.networks import SimplexElement
>>> SimplexElement('triangles').age
0.0
Source code in src/skcomplex/systems/mathematics/networks/_hypergraphs.py
49
50
51
52
53
54
55
56
57
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.systems.mathematics.networks import SizeBiasedMoveInteraction
>>> SizeBiasedMoveInteraction('moves', target='nodes').target
'nodes'
Source code in src/skcomplex/systems/mathematics/networks/_hypergraphs.py
154
155
156
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(label)
    self.target = target

SuperposedNodeElement(label, visual=DEGREE_VISUAL, *, degree=0.0, clustering=0.0)

Bases: Element

Node of a superposed network, with its degree and its clustering.

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.

0.0
clustering Annotated[float, StateSpec(domain=closed(0, 1))]

The share of the pairs of neighbours of the node that are joined.

0.0

Examples:

>>> from skcomplex.systems.mathematics.networks import SuperposedNodeElement
>>> SuperposedNodeElement('nodes').clustering
0.0
Source code in src/skcomplex/systems/mathematics/networks/_superposition.py
46
47
48
49
50
51
52
53
54
55
56
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = DEGREE_VISUAL,
    *,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    clustering: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.degree = degree
    self.clustering = clustering

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 system 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.systems.mathematics.networks import TriadVertexElement
>>> TriadVertexElement('nodes', triad=0.5).triad
0.5
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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.systems.mathematics.networks import TriadicRegulationInteraction
>>> TriadicRegulationInteraction('regulation', nodes='nodes', links='links').size
3
Source code in src/skcomplex/systems/mathematics/networks/_triadic.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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 system 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.systems.mathematics.networks import VertexElement
>>> VertexElement('nodes').age
0.0
Source code in src/skcomplex/systems/mathematics/common/_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.systems.mathematics.networks import WalkerDensityElement
>>> WalkerDensityElement('node', D=4.0).D
4.0
Source code in src/skcomplex/systems/mathematics/networks/_hypergraphs.py
86
87
88
89
90
91
92
93
94
95
96
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, fitness=False, random_state=0)

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.

With fitness, every node draws a fitness eta uniformly in [0, 1] and new edges join nodes with probability proportional to eta k, the model of Bianconi and Barabási (2001) that Raman (2021, section 3.2.4) describes. The degree of a node then grows as t^beta(eta) with an exponent that rises with its fitness, so a fit late node overtakes older ones.

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
fitness bool

Whether the nodes draw a fitness that scales their attachment, the Bianconi-Barabási model.

False
random_state int | Generator | None

The seed or generator of the fitness of the seed nodes.

0

Returns:

Name Type Description
system System

The system, with the groups nodes and links and the arrival growth, and with fitness the state nodes.fitness.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import barabasi_albert
>>> trajectory = Simulation(barabasi_albert(13), random_state=0).prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).sources.size)
23
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
@register_system(
    field='mathematics/networks',
    tags=('network', 'growth', 'preferential attachment', 'scale-free'),
    references=(
        'Barabási and Albert 1999',
        'Bianconi and Barabási 2001',
        'Argun et al. 2021',
        'Gros 2015',
        'Thurner et al. 2018',
        'Raman 2021',
    ),
    defaults={'until': 300},
)
def barabasi_albert(
    n_nodes: int = 303,
    *,
    edges: int = 2,
    seed_nodes: int = 3,
    fitness: bool = False,
    random_state: int | np.random.Generator | None = 0,
) -> 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.

    With `fitness`, every node draws a fitness `eta` uniformly in `[0, 1]` and new edges join nodes with probability
    proportional to `eta k`, the model of Bianconi and Barabási (2001) that Raman (2021, section 3.2.4) describes.
    The degree of a node then grows as `t^beta(eta)` with an exponent that rises with its fitness, so a fit late node
    overtakes older ones.

    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.

        fitness:
            Whether the nodes draw a fitness that scales their attachment, the Bianconi-Barabási model.

        random_state:
            The seed or generator of the fitness of the seed nodes.

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

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import barabasi_albert
        >>> trajectory = Simulation(barabasi_albert(13), random_state=0).prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).sources.size)
        23
    """
    space = NetworkSpace(nx.complete_graph(seed_nodes))
    capacity = edges * n_nodes + seed_nodes**2
    if not fitness:
        nodes = _nodes(VertexElement, n_nodes)
        growth = TopologyInteraction('growth', action='add-node', edges=edges, toward=_degree)
        return _grown(nodes, growth, space, capacity)
    seed_fitness = np.random.default_rng(random_state).uniform(0.0, 1.0, seed_nodes)
    nodes = _nodes(FitnessVertexElement, n_nodes, fitness=seed_fitness.tolist())
    growth = TopologyInteraction(
        'growth',
        action='add-node',
        edges=edges,
        values={'fitness': lambda rng: rng.uniform(0.0, 1.0)},
        toward=lambda node: node.fitness * neighbors(1),
    )
    return _grown(nodes, growth, space, capacity)

configuration_model(degrees=None, *, n_nodes=1000, exponent=2.5, min_degree=2, simple=True, random_state=0)

Return the configuration model, the maximally random network of a given degree sequence.

Every node gets as many half-links as its degree, and pairs of half-links drawn uniformly at random become links (Jensen 2023, section 8.5.1, and Thurner et al. 2018, section 4.5.1). Without a degree sequence the degrees are drawn from the power law P(k) ~ k^-exponent for k >= min_degree, cut at sqrt(n_nodes), and one half-link is added to a random node when their sum is odd. The pairing leaves self-loops and multilinks, which the rewiring of Thurner et al. removes while it keeps every degree. A neighbour has the mean degree <k²> / <k>, above the mean degree, the friendship paradox.

Parameters:

Name Type Description Default
degrees Sequence[int] | None

The degree of every node, or None for degrees drawn from the power law.

None
n_nodes int

The number of nodes of the drawn degrees.

1000
exponent float

The exponent of the power law of the drawn degrees.

2.5
min_degree int

The smallest drawn degree.

2
simple bool

Whether self-loops and multilinks are rewired away, or dropped, which lowers some degrees.

True
random_state int | Generator | None

The seed or generator of the degrees, the pairing, and the rewiring.

0

Returns:

Name Type Description
system System

The system, with the group nodes and their states degree and clustering.

Raises:

Type Description
ValidationError

The degree sum of a given sequence is odd, or rewiring cannot make the graph simple.

Examples:

>>> from skcomplex.systems.mathematics.networks import configuration_model
>>> system = configuration_model([3, 3, 2, 2, 2])
>>> system.elements[0].values['degree'].tolist()
[3.0, 3.0, 2.0, 2.0, 2.0]
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
@register_system(
    field='mathematics/networks',
    tags=('network', 'configuration model', 'random graph', 'degree sequence', 'friendship paradox'),
    references=('Molloy and Reed 1995', 'Jensen 2023', 'Thurner et al. 2018'),
    defaults={'until': 1, 'dt': 1},
)
def configuration_model(
    degrees: Sequence[int] | None = None,
    *,
    n_nodes: int = 1000,
    exponent: float = 2.5,
    min_degree: int = 2,
    simple: bool = True,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the configuration model, the maximally random network of a given degree sequence.

    Every node gets as many half-links as its degree, and pairs of half-links drawn uniformly at random become links
    (Jensen 2023, section 8.5.1, and Thurner et al. 2018, section 4.5.1). Without a degree sequence the degrees are
    drawn from the power law `P(k) ~ k^-exponent` for `k >= min_degree`, cut at `sqrt(n_nodes)`, and one half-link
    is added to a random node when their sum is odd. The pairing leaves self-loops and multilinks, which the rewiring
    of Thurner et al. removes while it keeps every degree. A neighbour has the mean degree `<k²> / <k>`, above the
    mean degree, the friendship paradox.

    Args:
        degrees:
            The degree of every node, or `None` for degrees drawn from the power law.

        n_nodes:
            The number of nodes of the drawn degrees.

        exponent:
            The exponent of the power law of the drawn degrees.

        min_degree:
            The smallest drawn degree.

        simple:
            Whether self-loops and multilinks are rewired away, or dropped, which lowers some degrees.

        random_state:
            The seed or generator of the degrees, the pairing, and the rewiring.

    Returns:
        system:
            The system, with the group `nodes` and their states `degree` and `clustering`.

    Raises:
        ValidationError:
            The degree sum of a given sequence is odd, or rewiring cannot make the graph simple.

    Examples:
        >>> from skcomplex.systems.mathematics.networks import configuration_model
        >>> system = configuration_model([3, 3, 2, 2, 2])
        >>> system.elements[0].values['degree'].tolist()
        [3.0, 3.0, 2.0, 2.0, 2.0]
    """
    rng = np.random.default_rng(random_state)
    if degrees is None:
        support = np.arange(min_degree, max(min_degree, int(np.sqrt(n_nodes))) + 1)
        weights = support.astype(float) ** -exponent
        drawn = rng.choice(support, size=n_nodes, p=weights / weights.sum())
        drawn[int(rng.integers(n_nodes))] += int(drawn.sum() % 2)
    else:
        drawn = np.asarray(degrees, dtype=np.int64)
        if drawn.sum() % 2:
            error_msg = f'The degrees sum to {int(drawn.sum())}. Give a degree sequence with an even sum.'
            raise ValidationError(error_msg)
    links = _matched_stubs(drawn, rng)
    links = _simple(links, rng) if simple else [link for link in links if link[0] != link[1]]
    graph = nx.Graph()
    graph.add_nodes_from(range(drawn.size))
    graph.add_edges_from(links)
    return System(elements=[_node_group(graph)], space=NetworkSpace(graph))

davidsen_ebel_bornholdt(n_nodes=200, *, turnover=0.1, links=None, random_state=0)

Return the Davidsen-Ebel-Bornholdt network, whose triadic closure and node turnover give high clustering.

The network starts as an Erdős-Rényi graph of n_nodes nodes and links links, n_nodes by default. Every step a node drawn uniformly joins two of its neighbours that are not yet joined, or, without such a pair, joins a node drawn uniformly. Then, with probability turnover, a node drawn uniformly leaves with its links and a new node takes its place with one link to a node drawn uniformly (Thurner et al. 2018, section 4.5.4.6). A closure always adds one link, and a turnover removes a node of mean degree k and adds one link, so the steady state has the mean degree k = 1 + 1 / turnover. Closing triangles keeps the clustering high, and since a node lies near many others in proportion to its degree, closure attaches preferentially, so hubs emerge as the turnover falls.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

200
turnover float

The probability that a node is replaced at a step.

0.1
links int | None

The number of links of the initial Erdős-Rényi graph, or None for n_nodes.

None
random_state int | Generator | None

The seed or generator of the initial graph.

0

Returns:

Name Type Description
system System

The system, with the groups nodes and links, the closure closure, and the turnover turnover.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import davidsen_ebel_bornholdt
>>> trajectory = Simulation(davidsen_ebel_bornholdt(50), random_state=0).prepare().run(until=20).trajectory_
>>> len(trajectory.network(-1).nodes)
50
Source code in src/skcomplex/systems/mathematics/networks/_davidsen_ebel_bornholdt.py
 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
120
121
122
@register_system(
    field='mathematics/networks',
    tags=('network', 'triadic closure', 'clustering', 'node turnover', 'non-growing network'),
    references=('Davidsen, Ebel, and Bornholdt 2002', 'Thurner et al. 2018'),
    defaults={'until': 10000},
)
def davidsen_ebel_bornholdt(
    n_nodes: int = 200,
    *,
    turnover: float = 0.1,
    links: int | None = None,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Davidsen-Ebel-Bornholdt network, whose triadic closure and node turnover give high clustering.

    The network starts as an Erdős-Rényi graph of `n_nodes` nodes and `links` links, `n_nodes` by default. Every step a
    node drawn uniformly joins two of its neighbours that are not yet joined, or, without such a pair, joins a node
    drawn uniformly. Then, with probability `turnover`, a node drawn uniformly leaves with its links and a new node
    takes its place with one link to a node drawn uniformly (Thurner et al. 2018, section 4.5.4.6). A closure always
    adds one link, and a turnover removes a node of mean degree `k` and adds one link, so the steady state has the
    mean degree `k = 1 + 1 / turnover`. Closing triangles keeps the clustering high, and since a node lies near many
    others in proportion to its degree, closure attaches preferentially, so hubs emerge as the turnover falls.

    Args:
        n_nodes:
            The number of nodes.

        turnover:
            The probability that a node is replaced at a step.

        links:
            The number of links of the initial Erdős-Rényi graph, or `None` for `n_nodes`.

        random_state:
            The seed or generator of the initial graph.

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

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import davidsen_ebel_bornholdt
        >>> trajectory = Simulation(davidsen_ebel_bornholdt(50), random_state=0).prepare().run(until=20).trajectory_
        >>> len(trajectory.network(-1).nodes)
        50
    """
    rng = np.random.default_rng(random_state)
    graph = nx.gnm_random_graph(n_nodes, n_nodes if links is None else links, seed=rng)
    closure = TopologyInteraction('closure', target='nodes', action='close-triangle', one=True)
    replacement = TopologyInteraction(
        'turnover',
        target='nodes',
        action='replace-node',
        one=True,
        probability=turnover,
    )
    capacity = graph.number_of_edges() + int(2 * n_nodes * (1 + 1 / turnover))
    return _system(n_nodes, NetworkSpace(graph), capacity, [closure, replacement])

duplication_deletion(n_nodes=100, *, kill=1.0, inherit=0.5, parent=0.5, new=0.01, density=0.05, random_state=0)

Return the steady-state duplication-deletion network of Jensen, whose connectance settles at Eq. 8.84.

The network starts as an Erdős-Rényi graph of n_nodes nodes and the link probability density. Every step a node drawn uniformly leaves with its links with probability kill, and a parent drawn uniformly among the nodes left is duplicated: the offspring inherits each link of the parent with probability inherit, joins the parent with probability parent, and joins every node the parent does not join with probability new (Jensen 2023, section 8.5.1). With kill = 1 the network keeps N nodes, and its connectance converges to C = (new (N - 2) + parent) / (N - 1 + (new - inherit) (N - 2)), Eq. 8.84. Accurate duplication, with inherit near 1 and new near 0, turns the binomial degrees of noisy duplication into a cut-off power law. With kill < 1 the network grows by 1 - kill nodes per step on average, up to twice its initial size.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

100
kill float

The probability that the drawn node leaves at a step.

1.0
inherit float

The probability that the offspring inherits a link of its parent.

0.5
parent float

The probability that the offspring joins its parent.

0.5
new float

The probability that the offspring joins a node its parent does not join.

0.01
density float

The link probability of the initial Erdős-Rényi graph.

0.05
random_state int | Generator | None

The seed or generator of the initial graph.

0

Returns:

Name Type Description
system System

The system, with the groups nodes and links, the deletion death, and the duplication birth.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import duplication_deletion
>>> trajectory = Simulation(duplication_deletion(30), random_state=0).prepare().run(until=20).trajectory_
>>> len(trajectory.network(-1).nodes)
30
Source code in src/skcomplex/systems/mathematics/networks/_duplication_deletion.py
 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
@register_system(
    field='mathematics/networks',
    tags=('network', 'duplication', 'deletion', 'evolution', 'protein interaction network', 'connectance'),
    references=('Jensen 2023', 'Sole et al. 2002'),
    defaults={'until': 4000},
)
def duplication_deletion(
    n_nodes: int = 100,
    *,
    kill: float = 1.0,
    inherit: float = 0.5,
    parent: float = 0.5,
    new: float = 0.01,
    density: float = 0.05,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the steady-state duplication-deletion network of Jensen, whose connectance settles at Eq. 8.84.

    The network starts as an Erdős-Rényi graph of `n_nodes` nodes and the link probability `density`. Every step a node
    drawn uniformly leaves with its links with probability `kill`, and a parent drawn uniformly among the nodes left is
    duplicated: the offspring inherits each link of the parent with probability `inherit`, joins the parent with
    probability `parent`, and joins every node the parent does not join with probability `new` (Jensen 2023, section
    8.5.1). With `kill = 1` the network keeps `N` nodes, and its connectance converges to
    `C = (new (N - 2) + parent) / (N - 1 + (new - inherit) (N - 2))`, Eq. 8.84. Accurate duplication, with `inherit`
    near 1 and `new` near 0, turns the binomial degrees of noisy duplication into a cut-off power law. With `kill < 1`
    the network grows by `1 - kill` nodes per step on average, up to twice its initial size.

    Args:
        n_nodes:
            The number of nodes.

        kill:
            The probability that the drawn node leaves at a step.

        inherit:
            The probability that the offspring inherits a link of its parent.

        parent:
            The probability that the offspring joins its parent.

        new:
            The probability that the offspring joins a node its parent does not join.

        density:
            The link probability of the initial Erdős-Rényi graph.

        random_state:
            The seed or generator of the initial graph.

    Returns:
        system:
            The system, with the groups `nodes` and `links`, the deletion `death`, and the duplication `birth`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import duplication_deletion
        >>> trajectory = Simulation(duplication_deletion(30), random_state=0).prepare().run(until=20).trajectory_
        >>> len(trajectory.network(-1).nodes)
        30
    """
    graph = nx.gnp_random_graph(n_nodes, density, seed=np.random.default_rng(random_state))
    death = TopologyInteraction('death', target='nodes', action='remove-node', one=True, probability=kill)
    birth = TopologyInteraction(
        'birth',
        target='nodes',
        action='duplicate-node',
        one=True,
        inherit=inherit,
        parent=parent,
        independent=True,
        toward=lambda node: new,
    )
    capacity = n_nodes * (2 * n_nodes - 1)
    return _system(2 * n_nodes, NetworkSpace(graph), capacity, [death, birth])

explosive_percolation(n_nodes=1000, *, candidates=2)

Return explosive percolation, where the Achlioptas product rule delays and sharpens the giant component.

The network starts with n_nodes isolated nodes, and every step adds one edge. It draws candidates edges, each between two distinct nodes not yet joined drawn uniformly, and adds the one whose product of the sizes of the components it joins is smallest, the product rule of Achlioptas, D'Souza, and Spencer (2009). A map reads the size of the component of every node at the start of every step. With one candidate the edges are those of an Erdős-Rényi graph, whose giant component grows continuously from t = 1/2 edges per node. With two candidates the rule suppresses large components until the giant component jumps in a narrow window near t = 0.888.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

1000
candidates int

The number of candidate edges of every step.

2

Returns:

Name Type Description
system System

The system, with the size of the component of every node nodes.size.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import explosive_percolation
>>> trajectory = Simulation(explosive_percolation(100), random_state=0).prepare().run(until=50).trajectory_
>>> len(trajectory.network(-1).sources)
50
Source code in src/skcomplex/systems/mathematics/networks/_explosive.py
 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
@register_system(
    field='mathematics/networks',
    tags=('network', 'percolation', 'explosive percolation', 'Achlioptas process', 'giant component'),
    references=("Achlioptas, D'Souza, and Spencer 2009", 'Artime et al. 2025'),
    defaults={'until': 1000},
    observables=_largest_observables,
)
def explosive_percolation(n_nodes: int = 1000, *, candidates: int = 2) -> System:
    """Return explosive percolation, where the Achlioptas product rule delays and sharpens the giant component.

    The network starts with `n_nodes` isolated nodes, and every step adds one edge. It draws `candidates` edges, each
    between two distinct nodes not yet joined drawn uniformly, and adds the one whose product of the sizes of the
    components it joins is smallest, the product rule of Achlioptas, D'Souza, and Spencer (2009). A map reads the size
    of the component of every node at the start of every step. With one candidate the edges are those of an
    Erdős-Rényi graph, whose giant component grows continuously from `t = 1/2` edges per node. With two candidates the
    rule suppresses large components until the giant component jumps in a narrow window near `t = 0.888`.

    Args:
        n_nodes:
            The number of nodes.

        candidates:
            The number of candidate edges of every step.

    Returns:
        system:
            The system, with the size of the component of every node `nodes.size`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import explosive_percolation
        >>> trajectory = Simulation(explosive_percolation(100), random_state=0).prepare().run(until=50).trajectory_
        >>> len(trajectory.network(-1).sources)
        50
    """
    nodes = GroupElement('nodes', element=ClusterNodeElement, on='sites', variable=True, capacity=n_nodes)
    links = GroupElement('links', element=Element, on='edges', variable=True, capacity=2 * n_nodes)
    interactions = [
        MapInteraction('size', target='nodes', state='size', function=lambda s: components(1, reduce='size')),
        TopologyInteraction('linking', action='add-edge', candidates=candidates, score=_get_product),
    ]
    space = NetworkSpace(nx.empty_graph(n_nodes))
    return System(elements=[nodes, links], interactions=interactions, space=space, time=DiscreteTime())

graphlet_arrival(n_nodes=602, *, alpha=0.3)

Return growth by the arrival of three-node graphlets, a tree whose degree exponent rises with alpha.

The network starts from one edge, and every step a graphlet of a central node and two peripheral nodes joined to it arrives and merges one of its nodes with an existing node drawn with probability proportional to its degree: a peripheral node with probability alpha, so the existing node gains the central node, which carries the other peripheral node, and the central node otherwise, so the existing node gains both peripheral nodes (Filkov et al. 2009, as Raman 2021, section 3.2.4.2, describes). Every step adds two nodes and two edges, and the shares of nodes of degree 1 and 2 are 2 (2 - alpha) / 5 and alpha (7 - alpha) / 15.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the network holds room for.

602
alpha float

The probability that the graphlet attaches by a peripheral node.

0.3

Returns:

Name Type Description
system System

The system, with the groups nodes and links and the arrivals growth and graphlet.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import graphlet_arrival
>>> trajectory = Simulation(graphlet_arrival(22), random_state=0).prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).nodes.size), int(trajectory.network(-1).sources.size)
(22, 21)
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
@register_system(
    field='mathematics/networks',
    tags=('network', 'growth', 'graphlet', 'preferential attachment', 'tree'),
    references=('Filkov et al. 2009', 'Raman 2021'),
    defaults={'until': 300},
)
def graphlet_arrival(n_nodes: int = 602, *, alpha: float = 0.3) -> System:
    """Return growth by the arrival of three-node graphlets, a tree whose degree exponent rises with `alpha`.

    The network starts from one edge, and every step a graphlet of a central node and two peripheral nodes joined to it
    arrives and merges one of its nodes with an existing node drawn with probability proportional to its degree: a
    peripheral node with probability `alpha`, so the existing node gains the central node, which carries the other
    peripheral node, and the central node otherwise, so the existing node gains both peripheral nodes (Filkov et al.
    2009, as Raman 2021, section 3.2.4.2, describes). Every step adds two nodes and two edges, and the shares of nodes
    of degree 1 and 2 are `2 (2 - alpha) / 5` and `alpha (7 - alpha) / 15`.

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

        alpha:
            The probability that the graphlet attaches by a peripheral node.

    Returns:
        system:
            The system, with the groups `nodes` and `links` and the arrivals `growth` and `graphlet`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import graphlet_arrival
        >>> trajectory = Simulation(graphlet_arrival(22), random_state=0).prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).nodes.size), int(trajectory.network(-1).sources.size)
        (22, 21)
    """
    nodes = _nodes(VertexElement, n_nodes)
    growth = TopologyInteraction('growth', action='add-node', toward=_degree)
    graphlet = TopologyInteraction(
        'graphlet',
        action='add-node',
        values={'age': 0.0},
        toward=lambda node: alpha * where(node.age < JOINING_AGE, 1, 0)
        + (1 - alpha) * where(node.age > JOINING_AGE, 1, 0) * neighbors(where(node.other.age < JOINING_AGE, 1, 0)),
    )
    return _grown(nodes, growth, NetworkSpace(nx.path_graph(2)), n_nodes, [graphlet])

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 system, with the groups nodes, links, and triangles, and the arrival growth.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import growing_simplicial_complex
>>> simulation = Simulation(growing_simplicial_complex(13), random_state=0).prepare()
>>> simulation.run(until=10).trajectory_.network(-1).hyperedges.shape
(11, 3)
Source code in src/skcomplex/systems/mathematics/networks/_hypergraphs.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
@register_system(
    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 system, with the groups `nodes`, `links`, and `triangles`, and the arrival `growth`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import growing_simplicial_complex
        >>> simulation = Simulation(growing_simplicial_complex(13), random_state=0).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=Element, 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 system, with the groups nodes and links and the arrival growth.

Examples:

>>> from skcomplex.systems.mathematics.networks import holme_kim
>>> holme_kim(20).elements[0].element.__name__
'TriadVertexElement'
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
@register_system(
    field='mathematics/networks',
    tags=('network', 'growth', 'preferential attachment', 'clustering', 'triad formation'),
    references=('Holme and Kim 2002', 'Downey 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 system, with the groups `nodes` and `links` and the arrival `growth`.

    Examples:
        >>> from skcomplex.systems.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 system, with the probabilities nodes.p and the weights nodes.D.

Examples:

>>> import xgi
>>> from skcomplex.systems.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/systems/mathematics/networks/_hypergraphs.py
174
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
224
225
226
227
228
229
230
@register_system(
    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 system, with the probabilities `nodes.p` and the weights `nodes.D`.

    Examples:
        >>> import xgi
        >>> from skcomplex.systems.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.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')])

interdependent_network_cascade(n_nodes=2000, *, mean_degree=4.0, p=0.8, random_state=0)

Return the cascade of failures of Buldyrev et al. between two networks whose nodes depend on each other.

Two Erdős-Rényi networks of the mean degree mean_degree share their nodes, every node of one depending on the node of the same index in the other, so a node is one pair of dependent nodes. An attack leaves every node functional with probability p. At even steps the nodes outside the giant component of the functional nodes of the first network fail, and at odd steps those outside that of the second network, until the cascade stops at the mutual giant component (Buldyrev et al. 2010, as Artime et al. 2025, section 5, Eqs. 9 to 11, review it). Its share S solves S = p (1 - exp(-k S))², which has a solution above 0 only for p above 2.4554 / k, where the networks collapse at once in a discontinuous transition.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes of every network.

2000
mean_degree float

The mean degree k of every network.

4.0
p float

The share of nodes the attack leaves functional.

0.8
random_state int | Generator | None

The seed or generator of the networks and the attack.

0

Returns:

Name Type Description
system System

The system, with the functional nodes nodes.active over the graph of the first network and the layer dependent of the second.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import interdependent_network_cascade
>>> system = interdependent_network_cascade(200, p=0.3)
>>> int(Simulation(system).prepare().run(until=20).trajectory_.values('nodes.active')[-1].sum())
0
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
@register_system(
    field='mathematics/networks',
    tags=('network', 'interdependent networks', 'cascade', 'percolation', 'multilayer'),
    references=('Buldyrev et al. 2010', 'Artime et al. 2025'),
    defaults={'until': 40},
)
def interdependent_network_cascade(
    n_nodes: int = 2000,
    *,
    mean_degree: float = 4.0,
    p: float = 0.8,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the cascade of failures of Buldyrev et al. between two networks whose nodes depend on each other.

    Two Erdős-Rényi networks of the mean degree `mean_degree` share their nodes, every node of one depending on the
    node of the same index in the other, so a node is one pair of dependent nodes. An attack leaves every node
    functional with probability `p`. At even steps the nodes outside the giant component of the functional nodes of
    the first network fail, and at odd steps those outside that of the second network, until the cascade stops at the
    mutual giant component (Buldyrev et al. 2010, as Artime et al. 2025, section 5, Eqs. 9 to 11, review it). Its
    share `S` solves `S = p (1 - exp(-k S))²`, which has a solution above 0 only for `p` above `2.4554 / k`, where the
    networks collapse at once in a discontinuous transition.

    Args:
        n_nodes:
            The number of nodes of every network.

        mean_degree:
            The mean degree `k` of every network.

        p:
            The share of nodes the attack leaves functional.

        random_state:
            The seed or generator of the networks and the attack.

    Returns:
        system:
            The system, with the functional nodes `nodes.active` over the graph of the first network and the layer
            `dependent` of the second.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import interdependent_network_cascade
        >>> system = interdependent_network_cascade(200, p=0.3)
        >>> int(Simulation(system).prepare().run(until=20).trajectory_.values('nodes.active')[-1].sum())
        0
    """
    rng = np.random.default_rng(random_state)
    probability = mean_degree / (n_nodes - 1)
    first = nx.gnp_random_graph(n_nodes, probability, seed=int(rng.integers(2**31)))
    second = LayerSpec('dependent', nx.gnp_random_graph(n_nodes, probability, seed=int(rng.integers(2**31))))
    space = NetworkSpace(first, layers=[second])
    functional = (rng.uniform(size=n_nodes) < p).astype(float)
    nodes = LatticeElement('nodes', element=DependentNodeElement, space=space, values={'active': functional})

    def _get_cascade(s: SimpleNamespace) -> object:
        """Return the activity of a node after the failures of the network of the step."""
        both = s.target.active * s.target.other.active
        first_giant = components(both, reduce='giant')
        second_giant = components(both, over=second, reduce='giant')
        return s.target.active * where(mod(s.t, 2) < 1, first_giant, second_giant)

    cascade = MapInteraction('cascade', target='nodes', state='active', function=_get_cascade)
    return System(elements=[nodes], interactions=[cascade], time=DiscreteTime())

kronecker_graph(first=None, second=None, *, random_state=0)

Return the Kronecker graph of two graphs, whose adjacency matrix is the Kronecker product of theirs.

The node (i, j) joins (k, l) when i joins k in the first graph and j joins l in the second (Gros 2015, exercise of chapter 1). The degree of (i, j) is the product of the degrees of i and j, so the mean degree is the product of the mean degrees, and the degree distribution is the distribution of the products.

Parameters:

Name Type Description Default
first Graph | None

The first graph, or None for a Barabási-Albert graph of 30 nodes with two edges per new node.

None
second Graph | None

The second graph, or None for a Barabási-Albert graph of 20 nodes with one edge per new node.

None
random_state int | Generator | None

The seed or generator of the default graphs.

0

Returns:

Name Type Description
system System

The system, with the group nodes and their states degree and clustering.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.mathematics.networks import kronecker_graph
>>> kronecker_graph(nx.path_graph(3), nx.path_graph(2)).elements[0].prepare().size_
6
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
@register_system(
    field='mathematics/networks',
    tags=('network', 'kronecker product', 'random graph', 'degree distribution'),
    references=('Leskovec et al. 2010', 'Gros 2015'),
    defaults={'until': 1, 'dt': 1},
)
def kronecker_graph(
    first: nx.Graph | None = None,
    second: nx.Graph | None = None,
    *,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Kronecker graph of two graphs, whose adjacency matrix is the Kronecker product of theirs.

    The node `(i, j)` joins `(k, l)` when `i` joins `k` in the first graph and `j` joins `l` in the second (Gros
    2015, exercise of chapter 1). The degree of `(i, j)` is the product of the degrees of `i` and `j`, so the mean
    degree is the product of the mean degrees, and the degree distribution is the distribution of the products.

    Args:
        first:
            The first graph, or `None` for a Barabási-Albert graph of 30 nodes with two edges per new node.

        second:
            The second graph, or `None` for a Barabási-Albert graph of 20 nodes with one edge per new node.

        random_state:
            The seed or generator of the default graphs.

    Returns:
        system:
            The system, with the group `nodes` and their states `degree` and `clustering`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.mathematics.networks import kronecker_graph
        >>> kronecker_graph(nx.path_graph(3), nx.path_graph(2)).elements[0].prepare().size_
        6
    """
    rng = np.random.default_rng(random_state)
    first = nx.barabasi_albert_graph(30, 2, seed=int(rng.integers(2**31))) if first is None else first
    second = nx.barabasi_albert_graph(20, 1, seed=int(rng.integers(2**31))) if second is None else second
    graph = nx.convert_node_labels_to_integers(nx.tensor_product(first, second), label_attribute='pair')
    return System(elements=[_node_group(graph)], space=NetworkSpace(graph))

motter_lai_overload(graph=None, *, tolerance=0.2, attack='load', random_state=0)

Return the cascade of overload failures of Motter and Lai after one node is removed.

The load of a node is its betweenness, the number of shortest paths between other pairs of nodes that pass through it, and its capacity is (1 + tolerance) times its load on the intact network (Motter and Lai 2002, as Artime et al. 2025, section 5, review it). The attack removes one node at the first step, the node of the largest load, of the largest degree, or a random node. Every later step the loads are those of the network present, and every node whose load exceeds its capacity fails at once, until no node is overloaded. A heterogeneous network loses most of its largest component after an attack on a node of large load and a low tolerance, and keeps it after a random failure.

Parameters:

Name Type Description Default
graph Graph | None

The NetworkX graph, or None for a Barabási-Albert graph of 300 nodes with two edges per new node.

None
tolerance float

The spare capacity alpha of every node, as a share of its initial load.

0.2
attack str

'load' for the node of the largest load, 'degree' for that of the largest degree, or 'random'.

'load'
random_state int | Generator | None

The seed or generator of the graph and of the random attack.

0

Returns:

Name Type Description
system System

The system, with the nodes nodes, their load, capacity, and the size of the largest component giant, and the links links.

Raises:

Type Description
ValidationError

The attack is unknown.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import motter_lai_overload
>>> trajectory = Simulation(motter_lai_overload(tolerance=0.1)).prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).nodes.size) < 300
True
Source code in src/skcomplex/systems/mathematics/networks/_overload.py
 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
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
@register_system(
    field='mathematics/networks',
    tags=('network', 'robustness', 'cascade', 'betweenness', 'overload'),
    references=('Motter and Lai 2002', 'Artime et al. 2025'),
    defaults={'until': 20},
)
def motter_lai_overload(
    graph: nx.Graph | None = None,
    *,
    tolerance: float = 0.2,
    attack: str = 'load',
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the cascade of overload failures of Motter and Lai after one node is removed.

    The load of a node is its betweenness, the number of shortest paths between other pairs of nodes that pass
    through it, and its capacity is `(1 + tolerance)` times its load on the intact network (Motter and Lai 2002, as
    Artime et al. 2025, section 5, review it). The attack removes one node at the first step, the node of the largest
    load, of the largest degree, or a random node. Every later step the loads are those of the network present, and
    every node whose load exceeds its capacity fails at once, until no node is overloaded. A heterogeneous network
    loses most of its largest component after an attack on a node of large load and a low tolerance, and keeps it
    after a random failure.

    Args:
        graph:
            The NetworkX graph, or `None` for a Barabási-Albert graph of 300 nodes with two edges per new node.

        tolerance:
            The spare capacity `alpha` of every node, as a share of its initial load.

        attack:
            `'load'` for the node of the largest load, `'degree'` for that of the largest degree, or `'random'`.

        random_state:
            The seed or generator of the graph and of the random attack.

    Returns:
        system:
            The system, with the nodes `nodes`, their `load`, `capacity`, and the size of the largest component
            `giant`, and the links `links`.

    Raises:
        ValidationError:
            The attack is unknown.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import motter_lai_overload
        >>> trajectory = Simulation(motter_lai_overload(tolerance=0.1)).prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).nodes.size) < 300
        True
    """
    if attack not in ATTACKS:
        error_msg = f'Unknown attack {attack!r}. Use one of {", ".join(ATTACKS)}.'
        raise ValidationError(error_msg)
    rng = np.random.default_rng(random_state)
    graph = nx.barabasi_albert_graph(300, 2, seed=int(rng.integers(2**31))) if graph is None else graph
    graph = nx.convert_node_labels_to_integers(graph)
    found = nx.betweenness_centrality(graph, normalized=False)
    loads = np.asarray([found[node] for node in graph.nodes], dtype=float)
    degrees = np.asarray([degree for _, degree in graph.degree()], dtype=float)
    target = {
        'load': int(np.argmax(loads)),
        'degree': int(np.argmax(degrees)),
        'random': int(rng.integers(len(loads))),
    }[attack]
    capacities = (1 + tolerance) * loads
    capacities[target] = -1.0
    nodes = GroupElement(
        'nodes',
        element=LoadedNodeElement,
        on='sites',
        variable=True,
        capacity=len(loads),
        values={'load': loads, 'capacity': capacities, 'giant': float(len(loads))},
    )
    links = GroupElement('links', element=Element, on='edges', variable=True, capacity=graph.number_of_edges())
    return System(
        elements=[nodes, links],
        interactions=[
            MapInteraction('load', target='nodes', state='load', function=lambda s: shortest_paths(1)),
            MapInteraction('giant', target='nodes', state='giant', function=lambda s: components(1, reduce='largest')),
            TopologyInteraction('overload', target='nodes', action='remove-node', condition=_is_overloaded),
        ],
        space=NetworkSpace(graph),
        time=DiscreteTime(),
    )

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 system, with the groups nodes and links.

Raises:

Type Description
ValidationError

The strategy is unknown.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import network_attack
>>> trajectory = Simulation(network_attack(50), random_state=0).prepare().run(until=3).trajectory_
>>> int(trajectory.network(-1).nodes.size) < 50
True
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
@register_system(
    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 system, with the groups `nodes` and `links`.

    Raises:
        ValidationError:
            The strategy is unknown.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import network_attack
        >>> trajectory = Simulation(network_attack(50), random_state=0).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 system, with the contents nodes.u.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.mathematics.networks import network_diffusion
>>> network_diffusion(nx.path_graph(4)).elements[0].prepare().size_
4
Source code in src/skcomplex/systems/mathematics/networks/_processes.py
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
224
225
226
227
228
229
230
231
232
233
234
235
@register_system(
    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 system, with the contents `nodes.u`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.mathematics.networks import network_diffusion
        >>> network_diffusion(nx.path_graph(4)).elements[0].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 system, with the group walkers.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.mathematics.networks import network_random_walk
>>> network_random_walk(nx.cycle_graph(6), n_walkers=3).elements[0].prepare().size_
3
Source code in src/skcomplex/systems/mathematics/networks/_processes.py
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
196
@register_system(
    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 system, with the group `walkers`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.mathematics.networks import network_random_walk
        >>> network_random_walk(nx.cycle_graph(6), n_walkers=3).elements[0].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 system 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 system 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 system, the community detection, and the null graphs.

0

Returns:

Name Type Description
system System

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

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.mathematics.networks import network_structure_analysis
>>> system = network_structure_analysis(nx.karate_club_graph(), reference='club', null_samples=0)
>>> system.elements[0].prepare().size_
34
Source code in src/skcomplex/systems/mathematics/networks/_structure.py
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
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
@register_system(
    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 system 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 system 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 system, the community detection, and the null graphs.

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

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.mathematics.networks import network_structure_analysis
        >>> system = network_structure_analysis(nx.karate_club_graph(), reference='club', null_samples=0)
        >>> system.elements[0].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)
        .prepare(network)
        .run()
    )
    summary = NetworkAnalysis('summary').prepare(network).run()
    clustering = nx.clustering(network)
    betweenness = NetworkAnalysis('centrality', measure='betweenness').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.prepare(network).run().z_scores_[0]['triangle']
    measures = NetworkMeasuresElement(
        'measures',
        path_length=float(summary.path_length_[0]),
        assortativity=float(NetworkAnalysis('assortativity').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))

network_superposition(n_nodes=100, *, n_subgraphs=20, exponent=2.0, low=0.005, high=0.1, random_state=0)

Return the network superposition model, a union of Erdős-Rényi graphs whose linking probabilities are drawn.

Every one of n_subgraphs subgraphs of n_nodes nodes draws one linking probability p from Q(p) ~ p^-exponent on [low, high] and links each pair of its nodes with probability p, and the network is their union (Thurner et al. 2018, section 4.5.2). Its degrees mix the Poisson laws of the subgraphs, P(k) = ∫ dp P(k | N p) Q(p), Eq. 4.34, so a power law of the probabilities gives a power law of the degrees up to the largest mean degree N high. The book draws p for every attempted link, which gives one Erdős-Rényi graph of the mean probability, so the probability is drawn once per subgraph, the reading of Eq. 4.34, like the cells of superstatistics.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes of every subgraph.

100
n_subgraphs int

The number of subgraphs.

20
exponent float

The exponent of the distribution Q(p) of the linking probabilities.

2.0
low float

The smallest linking probability.

0.005
high float

The largest linking probability.

0.1
random_state int | Generator | None

The seed or generator of the linking probabilities and the links.

0

Returns:

Name Type Description
system System

The system, with the group nodes and their states degree and clustering.

Raises:

Type Description
ValidationError

The bounds of the linking probabilities are not 0 < low <= high <= 1.

Examples:

>>> from skcomplex.systems.mathematics.networks import network_superposition
>>> system = network_superposition(10, n_subgraphs=3)
>>> system.elements[0].values['degree'].size
30
Source code in src/skcomplex/systems/mathematics/networks/_superposition.py
 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
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
@register_system(
    field='mathematics/networks',
    tags=('network', 'superposition', 'random graph', 'superstatistics', 'degree distribution'),
    references=('Abe and Thurner 2005', 'Thurner et al. 2018'),
    defaults={'until': 1, 'dt': 1},
)
def network_superposition(
    n_nodes: int = 100,
    *,
    n_subgraphs: int = 20,
    exponent: float = 2.0,
    low: float = 0.005,
    high: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the network superposition model, a union of Erdős-Rényi graphs whose linking probabilities are drawn.

    Every one of `n_subgraphs` subgraphs of `n_nodes` nodes draws one linking probability `p` from
    `Q(p) ~ p^-exponent` on `[low, high]` and links each pair of its nodes with probability `p`, and the network is
    their union (Thurner et al. 2018, section 4.5.2). Its degrees mix the Poisson laws of the subgraphs,
    `P(k) = ∫ dp P(k | N p) Q(p)`, Eq. 4.34, so a power law of the probabilities gives a power law of the degrees up
    to the largest mean degree `N high`. The book draws `p` for every attempted link, which gives one Erdős-Rényi graph
    of the mean probability, so the probability is drawn once per subgraph, the reading of Eq. 4.34, like the cells of
    superstatistics.

    Args:
        n_nodes:
            The number of nodes of every subgraph.

        n_subgraphs:
            The number of subgraphs.

        exponent:
            The exponent of the distribution `Q(p)` of the linking probabilities.

        low:
            The smallest linking probability.

        high:
            The largest linking probability.

        random_state:
            The seed or generator of the linking probabilities and the links.

    Returns:
        system:
            The system, with the group `nodes` and their states `degree` and `clustering`.

    Raises:
        ValidationError:
            The bounds of the linking probabilities are not `0 < low <= high <= 1`.

    Examples:
        >>> from skcomplex.systems.mathematics.networks import network_superposition
        >>> system = network_superposition(10, n_subgraphs=3)
        >>> system.elements[0].values['degree'].size
        30
    """
    if not 0 < low <= high <= 1:
        error_msg = f'The linking probabilities lie in [{low}, {high}]. Give bounds with 0 < low <= high <= 1.'
        raise ValidationError(error_msg)
    rng = np.random.default_rng(random_state)
    graph = nx.Graph()
    for index, chance in enumerate(_linking_probabilities(n_subgraphs, exponent, (low, high), rng)):
        subgraph = nx.fast_gnp_random_graph(n_nodes, float(chance), seed=rng)
        graph.update(nx.relabel_nodes(subgraph, {node: index * n_nodes + node for node in subgraph}))
    clustering = nx.clustering(graph)
    nodes = GroupElement(
        'nodes',
        element=SuperposedNodeElement,
        size=graph.number_of_nodes(),
        values={
            'degree': np.asarray([graph.degree(node) for node in graph], dtype=float),
            'clustering': np.asarray([clustering[node] for node in graph], dtype=float),
        },
        location=list(graph),
    )
    return System(elements=[nodes], space=NetworkSpace(graph))

node_merging(n_nodes=500, *, edges=1, random_state=0)

Return the node-merging network of Kim et al., whose merging of linked nodes builds hubs without growth.

The network starts as a random tree of n_nodes nodes. Every step a node drawn uniformly merges with one of its neighbours drawn uniformly into one node that keeps the union of their links, and a new node joins edges nodes drawn uniformly, so the network keeps its size (Thurner et al. 2018, section 4.5.4.6). Merging adds the links of the neighbour to the node, so well-linked nodes absorb more links, and the degrees follow a fat-tailed, q-exponential distribution with hubs far above the degrees of a random graph.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

500
edges int

The number of links of every new node.

1
random_state int | Generator | None

The seed or generator of the initial tree.

0

Returns:

Name Type Description
system System

The system, with the groups nodes and links, the merging merging, and the arrival arrival.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import node_merging
>>> trajectory = Simulation(node_merging(50), random_state=0).prepare().run(until=20).trajectory_
>>> network = trajectory.network(-1)
>>> len(network.nodes), len(network.sources)
(50, 49)
Source code in src/skcomplex/systems/mathematics/networks/_node_merging.py
 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
@register_system(
    field='mathematics/networks',
    tags=('network', 'merging', 'edge contraction', 'scale-free', 'non-growing network'),
    references=('Kim, Trusina, Minnhagen, and Sneppen 2005', 'Thurner et al. 2018'),
    defaults={'until': 10000},
)
def node_merging(
    n_nodes: int = 500,
    *,
    edges: int = 1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the node-merging network of Kim et al., whose merging of linked nodes builds hubs without growth.

    The network starts as a random tree of `n_nodes` nodes. Every step a node drawn uniformly merges with one of its
    neighbours drawn uniformly into one node that keeps the union of their links, and a new node joins `edges` nodes
    drawn uniformly, so the network keeps its size (Thurner et al. 2018, section 4.5.4.6). Merging adds the links of
    the neighbour to the node, so well-linked nodes absorb more links, and the degrees follow a fat-tailed,
    q-exponential distribution with hubs far above the degrees of a random graph.

    Args:
        n_nodes:
            The number of nodes.

        edges:
            The number of links of every new node.

        random_state:
            The seed or generator of the initial tree.

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

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import node_merging
        >>> trajectory = Simulation(node_merging(50), random_state=0).prepare().run(until=20).trajectory_
        >>> network = trajectory.network(-1)
        >>> len(network.nodes), len(network.sources)
        (50, 49)
    """
    graph = nx.random_labeled_tree(n_nodes, seed=np.random.default_rng(random_state))
    merging = TopologyInteraction('merging', target='nodes', action='contract-edge', one=True)
    arrival = TopologyInteraction('arrival', action='add-node', edges=edges)
    capacity = 4 * edges * n_nodes
    return _system(n_nodes, NetworkSpace(graph), capacity, [merging, arrival])

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 system, with the ranks pages.rank.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.mathematics.networks import pagerank
>>> pagerank(nx.cycle_graph(5)).elements[0].prepare().size_
5
Source code in src/skcomplex/systems/mathematics/networks/_processes.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
@register_system(
    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 system, with the ranks `pages.rank`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.mathematics.networks import pagerank
        >>> pagerank(nx.cycle_graph(5)).elements[0].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])

popularity_similarity_network(n_nodes=300, *, edges=2, exponent=2.5, temperature=0.2, random_state=0)

Return popularity-similarity growth, scale-free and clustered, whose nodes attach by hyperbolic distance.

Every step a node joins at a uniform angle on the circle and the radius 2 ln t, every other node s moves to the radius b 2 ln s + (1 - b) 2 ln t with b = 1 / (exponent - 1), and the new node joins edges nodes drawn in proportion to the Fermi-Dirac probability of their hyperbolic distance x = r_s + r_t + 2 ln(theta / 2), at the temperature temperature. The degrees follow a power law of the given exponent, and the clustering falls as the temperature rises (Papadopoulos et al. 2012).

Parameters:

Name Type Description Default
n_nodes int

The number of nodes the network holds room for.

300
edges int

The number of edges of every new node, and one less than the nodes of the complete seed network.

2
exponent float

The exponent of the degree distribution, above 2.

2.5
temperature float

The temperature of the network, between 0 and 1.

0.2
random_state int | Generator | None

The seed or generator of the angles of the seed network.

0

Returns:

Name Type Description
system System

The system, with the groups nodes and links, the arrival growth, and the states nodes.angle, nodes.age, and nodes.degree.

Raises:

Type Description
ValidationError

The exponent is not above 2, or the temperature is not between 0 and 1.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import popularity_similarity_network
>>> system = popularity_similarity_network(13)
>>> trajectory = Simulation(system, random_state=0).prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).sources.size)
23
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
@register_system(
    field='mathematics/networks',
    tags=('network', 'growth', 'hyperbolic', 'popularity-similarity', 'scale-free', 'clustering'),
    references=(
        'Papadopoulos et al. 2012, arXiv 1106.0286',
        'Gabrielli, Garlaschelli, Patil, and Serrano 2024, arXiv 2412.12988',
    ),
    defaults={'until': 297},
)
def popularity_similarity_network(
    n_nodes: int = 300,
    *,
    edges: int = 2,
    exponent: float = 2.5,
    temperature: float = 0.2,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return popularity-similarity growth, scale-free and clustered, whose nodes attach by hyperbolic distance.

    Every step a node joins at a uniform angle on the circle and the radius `2 ln t`, every other node `s` moves to the
    radius `b 2 ln s + (1 - b) 2 ln t` with `b = 1 / (exponent - 1)`, and the new node joins `edges` nodes drawn in
    proportion to the Fermi-Dirac probability of their hyperbolic distance `x = r_s + r_t + 2 ln(theta / 2)`, at the
    temperature `temperature`. The degrees follow a power law of the given exponent, and the clustering falls as the
    temperature rises (Papadopoulos et al. 2012).

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

        edges:
            The number of edges of every new node, and one less than the nodes of the complete seed network.

        exponent:
            The exponent of the degree distribution, above 2.

        temperature:
            The temperature of the network, between 0 and 1.

        random_state:
            The seed or generator of the angles of the seed network.

    Returns:
        system:
            The system, with the groups `nodes` and `links`, the arrival `growth`, and the states `nodes.angle`,
            `nodes.age`, and `nodes.degree`.

    Raises:
        ValidationError:
            The exponent is not above 2, or the temperature is not between 0 and 1.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import popularity_similarity_network
        >>> system = popularity_similarity_network(13)
        >>> trajectory = Simulation(system, random_state=0).prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).sources.size)
        23
    """
    if exponent <= MIN_EXPONENT:
        error_msg = f'The degree exponent of the growth is {exponent!r}. Give an exponent above {MIN_EXPONENT}.'
        raise ValidationError(error_msg)
    if not 0 < temperature < 1:
        error_msg = f'The temperature of the growth is {temperature!r}. Give a temperature between 0 and 1.'
        raise ValidationError(error_msg)
    seed_nodes = edges + 1
    fading = 1 / (exponent - 1)
    angles = np.random.default_rng(random_state).uniform(0.0, 2 * np.pi, seed_nodes)
    ages = np.arange(seed_nodes - 1, -1, -1, dtype=float)
    nodes = _nodes(SimilarityVertexElement, n_nodes, age=ages.tolist(), angle=angles.tolist())
    growth = TopologyInteraction(
        'growth',
        action='add-node',
        edges=edges,
        values={'angle': lambda rng: rng.uniform(0.0, 2 * np.pi)},
        toward=lambda node: _get_similarity_weight(
            node,
            seed_nodes + 1,
            edges=edges,
            fading=fading,
            temperature=temperature,
        ),
    )
    return _grown(nodes, growth, NetworkSpace(nx.complete_graph(seed_nodes)), edges * n_nodes + seed_nodes**2)

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 system, 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.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import random_graph_ensembles
>>> system = random_graph_ensembles(40, ensemble='watts_strogatz', steps=5)
>>> trajectory = Simulation(system, random_state=0).prepare().run(until=5).trajectory_
>>> int(trajectory.network(-1).sources.size)
80
Source code in src/skcomplex/systems/mathematics/networks/_ensembles.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
@register_system(
    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 system, with the groups `nodes` and `links`, the map `degree`, and the interaction `linking` or
            `rewiring`.

    Raises:
        ValidationError:
            The ensemble is unknown.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import random_graph_ensembles
        >>> system = random_graph_ensembles(40, ensemble='watts_strogatz', steps=5)
        >>> trajectory = Simulation(system, random_state=0).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())

spatial_triadic_percolation(side=4.0, *, density=100.0, c=0.4, positive=0.2, negative=0.2, structural_range=0.25, regulatory_range=0.25, p=0.8, random_state=0)

Return spatial triadic percolation, where nearby signed regulators make the giant component form moving patterns.

The nodes lie uniformly on a square torus of side side with density nodes per unit area, and two nodes at the distance d are linked with probability c exp(-d / structural_range), a Waxman network, Eq. 1 of Millán et al. A node regulates a link positively with probability positive exp(-d / regulatory_range) and negatively with probability negative exp(-d / regulatory_range), never both, from its distance d to the midpoint of the link, Eq. 2. Every step of the paper runs in two phases. In the phase regulation a link is active with probability p when none of its negative regulators is active and one of its positive regulators is, or, for the rare link without positive regulators, when none of its negative regulators is active. In the phase percolation a node is active when it belongs to the giant component of the active links. All nodes and links start active.

The regulation is local, so the giant component forms clusters, stripes, and octopus-like patterns that move, and its size keeps changing instead of settling. At p = 0.8 with positive = negative the patterns blink: a pattern deactivates the links it regulates negatively and the next step activates the region around it, so the active nodes of consecutive steps are anti-correlated. For long ranges the model becomes the triadic percolation of Erdős-Rényi networks with many regulators per link.

Parameters:

Name Type Description Default
side float

The side of the square torus.

4.0
density float

The number of nodes per unit area.

100.0
c float

The prefactor of the probability of a structural link.

0.4
positive float

The prefactor of the probability of a positive regulation.

0.2
negative float

The prefactor of the probability of a negative regulation.

0.2
structural_range float

The typical length of the structural links, d0.

0.25
regulatory_range float

The typical length of the regulations, dr.

0.25
p float

The probability that a link the regulation allows is active.

0.8
random_state int | Generator | None

The seed or generator of the positions, the links, and the regulators.

0

Returns:

Name Type Description
system System

The system, with the activities nodes.active and links.active, and links.negatives and links.positives, the numbers of negative and positive regulators of every link, on a network whose nodes hold their positions.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import spatial_triadic_percolation
>>> system = spatial_triadic_percolation(1.0)
>>> trajectory = Simulation(system, random_state=0).prepare().run(until=2).trajectory_
>>> giant = trajectory.values('nodes.active').mean(axis=1)
>>> bool(giant[0] == 1.0 and giant[1] < 0.1)
True
Source code in src/skcomplex/systems/mathematics/networks/_spatial_triadic.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
@register_system(
    field='mathematics/networks',
    tags=('network', 'hypergraph', 'percolation', 'triadic interactions', 'spatial network', 'pattern formation'),
    references=('Millán, Sun, Torres, and Bianconi 2023', 'Waxman 1988'),
    defaults={'until': 100, 'dt': 1},
    observables=_giant_observables,
)
def spatial_triadic_percolation(
    side: float = 4.0,
    *,
    density: float = 100.0,
    c: float = 0.4,
    positive: float = 0.2,
    negative: float = 0.2,
    structural_range: float = 0.25,
    regulatory_range: float = 0.25,
    p: float = 0.8,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return spatial triadic percolation, where nearby signed regulators make the giant component form moving patterns.

    The nodes lie uniformly on a square torus of side `side` with `density` nodes per unit area, and two nodes at the
    distance `d` are linked with probability `c exp(-d / structural_range)`, a Waxman network, Eq. 1 of Millán et al.
    A node regulates a link positively with probability `positive exp(-d / regulatory_range)` and negatively with
    probability `negative exp(-d / regulatory_range)`, never both, from its distance `d` to the midpoint of the link,
    Eq. 2. Every step of the paper runs in two phases. In the phase `regulation` a link is active with probability `p`
    when none of its negative regulators is active and one of its positive regulators is, or, for the rare link
    without positive regulators, when none of its negative regulators is active. In the phase `percolation` a node is
    active when it belongs to the giant component of the active links. All nodes and links start active.

    The regulation is local, so the giant component forms clusters, stripes, and octopus-like patterns that move,
    and its size keeps changing instead of settling. At `p = 0.8` with `positive = negative` the patterns blink: a
    pattern deactivates the links it regulates negatively and the next step activates the region around it, so the
    active nodes of consecutive steps are anti-correlated. For long ranges the model becomes the triadic percolation of
    Erdős-Rényi networks with many regulators per link.

    Args:
        side:
            The side of the square torus.

        density:
            The number of nodes per unit area.

        c:
            The prefactor of the probability of a structural link.

        positive:
            The prefactor of the probability of a positive regulation.

        negative:
            The prefactor of the probability of a negative regulation.

        structural_range:
            The typical length of the structural links, `d0`.

        regulatory_range:
            The typical length of the regulations, `dr`.

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

        random_state:
            The seed or generator of the positions, the links, and the regulators.

    Returns:
        system:
            The system, with the activities `nodes.active` and `links.active`, and `links.negatives` and
            `links.positives`, the numbers of negative and positive regulators of every link, on a network whose nodes
            hold their positions.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import spatial_triadic_percolation
        >>> system = spatial_triadic_percolation(1.0)
        >>> trajectory = Simulation(system, random_state=0).prepare().run(until=2).trajectory_
        >>> giant = trajectory.values('nodes.active').mean(axis=1)
        >>> bool(giant[0] == 1.0 and giant[1] < 0.1)
        True
    """
    rng = np.random.default_rng(random_state)
    graph = _waxman_torus(round(density * side**2), side, c, structural_range, rng)
    hyperedges, negatives, positives = _spatial_regulators(graph, side, (positive, negative), regulatory_range, rng)
    space = NetworkSpace(graph, hyperedges=hyperedges)
    nodes = LatticeElement('nodes', element=PercolatingNodeElement, space=space)
    values = {'negatives': negatives, 'positives': positives}
    links = GroupElement('links', element=SignedLinkElement, space=space, on='edges', values=values)
    interactions = [
        SignedRegulationInteraction('regulation', nodes='nodes', links='links', p=p, phase='regulation'),
        PercolationInteraction('percolation', nodes='nodes', links='links', phase='percolation'),
    ]
    time = DiscreteTime(schedule=PhasesScheduleSpec((PhaseSpec('regulation'), PhaseSpec('percolation'))))
    return System(elements=[nodes, links], interactions=interactions, time=time)

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 system, with the activities nodes.active and links.active and the signs links.sign.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.mathematics.networks import triadic_percolation
>>> system = triadic_percolation(200, p=0.8)
>>> trajectory = Simulation(system, random_state=0).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/systems/mathematics/networks/_triadic.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
@register_system(
    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 system, with the activities `nodes.active` and `links.active` and the signs `links.sign`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.mathematics.networks import triadic_percolation
        >>> system = triadic_percolation(200, p=0.8)
        >>> trajectory = Simulation(system, random_state=0).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 system, with the groups nodes and links and the arrival growth.

Examples:

>>> from skcomplex.systems.mathematics.networks import vertex_copying
>>> vertex_copying(20).interactions[0].edges
3
Source code in src/skcomplex/systems/mathematics/networks/_growth.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
@register_system(
    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 system, with the groups `nodes` and `links` and the arrival `growth`.

    Examples:
        >>> from skcomplex.systems.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])