py3plex Core Model

This guide explains how py3plex represents multilayer networks internally and how to work with the core data structures.

The multi_layer_network Class

The central data structure in py3plex is the multi_layer_network class, which provides a high-level API for working with multilayer networks while maintaining full compatibility with NetworkX.

Basic Structure

from py3plex.core import multinet

# Create a multilayer network
network = multinet.multi_layer_network()

# The underlying NetworkX graph
nx_graph = network.core_network  # MultiDiGraph or MultiGraph

# Layer management
layers = network.get_layers()  # List of layer names
layer_map = network.layer_name_map  # Layer name to ID mapping

Key Components

1. Core NetworkX Graph

At its heart, py3plex uses a NetworkX MultiDiGraph (for directed networks) or MultiGraph (for undirected networks) to store all nodes and edges.

# Access the underlying graph
G = network.core_network

# Use any NetworkX function
import networkx as nx
betweenness = nx.betweenness_centrality(G)

2. Layer Encoding

Nodes are internally encoded as (node_id, layer_id) tuples to distinguish the same logical node across different layers.

# Node 'Alice' in layer 'friends' is stored as ('Alice', 'friends')
network.add_nodes([('Alice', 'friends')])

# Access all node-layer pairs
for node_layer_pair in network.get_nodes():
    print(node_layer_pair)  # ('Alice', 'friends'), ('Bob', 'friends'), ...

3. Layer Mapping

py3plex maintains bidirectional mappings between layer names and internal layer IDs for efficient lookups.

# Get layer mapping
layer_map = network.layer_name_map
# {'friends': 0, 'colleagues': 1, ...}

4. Attributes

Node and edge attributes are stored directly in the underlying NetworkX graph and can be accessed and modified using standard NetworkX patterns.

# Add node with attributes
network.core_network.nodes[('Alice', 'friends')]['age'] = 30

# Add edge with attributes
network.add_edges([
    [('Alice', 'friends'), ('Bob', 'friends'), {'weight': 0.8, 'type': 'close'}]
])

Internal Representation

Node-Layer Pairs

The key concept in py3plex’s internal model is the node-layer pair: a tuple (node_id, layer_id) that uniquely identifies a node in a specific layer.

# Same logical node, different layers
alice_friends = ('Alice', 'friends')
alice_colleagues = ('Alice', 'colleagues')

# These are treated as different nodes internally
network.add_nodes([alice_friends, alice_colleagues])

This representation allows:

  • The same logical entity to appear in multiple layers

  • Different attributes per node-layer pair

  • Efficient layer-specific operations

Edge Types

Intra-Layer Edges

Edges connecting nodes within the same layer:

# Edge within 'friends' layer
network.add_edges([
    [('Alice', 'friends'), ('Bob', 'friends'), 1.0]
])

Inter-Layer Edges

Edges connecting nodes across different layers:

# Connect Alice across layers (identity edge)
network.add_edges([
    [('Alice', 'friends'), ('Alice', 'colleagues'), 1.0]
])

# Connect different nodes across layers
network.add_edges([
    [('Alice', 'friends'), ('Bob', 'colleagues'), 0.5]
])

Supra-Adjacency Matrix

The supra-adjacency matrix is a block matrix representation that encodes the entire multilayer network structure.

Mathematical Definition

For a multilayer network with \(L\) layers, the supra-adjacency matrix \(\\mathbf{S}\) is:

\[\begin{split}\\mathbf{S} = \\begin{bmatrix} A_1 & C_{12} & \\cdots & C_{1L} \\\\ C_{21} & A_2 & \\cdots & C_{2L} \\\\ \\vdots & \\vdots & \\ddots & \\vdots \\\\ C_{L1} & C_{L2} & \\cdots & A_L \\end{bmatrix}\end{split}\]

Where:

  • \(A_\\alpha\) is the adjacency matrix of layer \(\\alpha\) (intra-layer connections)

  • \(C_{\\alpha\\beta}\) is the coupling matrix between layers \(\\alpha\) and \(\\beta\) (inter-layer connections)

Construction

from py3plex.core import multinet

network = multinet.multi_layer_network()
# ... add nodes and edges ...

# Get supra-adjacency matrix (sparse by default for efficiency)
supra_adj = network.get_supra_adjacency_matrix(sparse=True)

# Dense version (for small networks only)
supra_adj_dense = network.get_supra_adjacency_matrix(sparse=False)

# Shape: (total_nodes, total_nodes)
# where total_nodes = sum of nodes across all layers
print(f"Shape: {supra_adj.shape}")

Visualization

Visualize the supra-adjacency matrix structure:

from py3plex.core import multinet, random_generators
import matplotlib.pyplot as plt

# Generate a small multilayer network
network = random_generators.random_multilayer_ER(
    num_nodes=50, num_layers=3, probability=0.1, directed=False
)

# Visualize the supra-adjacency matrix
network.visualize_matrix({"display": True})

The visualization shows the block structure with:

  • Diagonal blocks: intra-layer connections

  • Off-diagonal blocks: inter-layer connections

Network Construction

Creating Networks from Scratch

Method 1: Add edges directly

from py3plex.core import multinet

network = multinet.multi_layer_network()

# Add edges in list format: [source_node, source_layer, target_node, target_layer, weight]
network.add_edges([
    ['Alice', 'friends', 'Bob', 'friends', 1.0],
    ['Bob', 'friends', 'Carol', 'friends', 1.0],
    ['Alice', 'colleagues', 'Bob', 'colleagues', 1.0],
], input_type="list")

Method 2: Add nodes and edges separately

network = multinet.multi_layer_network()

# Add nodes explicitly
network.add_nodes([
    ('Alice', 'friends'),
    ('Bob', 'friends'),
    ('Alice', 'colleagues'),
    ('Bob', 'colleagues')
])

# Add edges between existing nodes
network.add_edges([
    [('Alice', 'friends'), ('Bob', 'friends'), 1.0],
    [('Alice', 'colleagues'), ('Bob', 'colleagues'), 1.0]
])

Method 3: Define layers first

network = multinet.multi_layer_network()

# Define layers explicitly
network.add_layer('friends')
network.add_layer('colleagues')

# Add content (nodes are created automatically with edges)
network.add_edges([
    ['Alice', 'friends', 'Bob', 'friends', 1.0]
], input_type="list")

Loading from Files

py3plex supports multiple input formats. See I/O and Serialization for complete documentation.

from py3plex.core import multinet

# From simple edge list (source target)
network = multinet.multi_layer_network().load_network(
    "data.edgelist",
    input_type="edgelist",
    directed=False
)

# From multilayer edge list (source target layer)
network = multinet.multi_layer_network().load_network(
    "data.multiedgelist",
    input_type="multiedgelist",
    directed=False
)

# From GraphML
network = multinet.multi_layer_network().load_network(
    "data.graphml",
    input_type="graphml"
)

# From NetworkX graph
import networkx as nx
G = nx.karate_club_graph()
network = multinet.multi_layer_network()
network.load_network_from_networkx(G)

Network Operations

Querying Network Elements

# Get all node-layer pairs
nodes = network.get_nodes(data=True)  # With attributes
nodes = network.get_nodes(data=False)  # Without attributes

# Get nodes in a specific layer
layer_nodes = network.get_nodes(layer='friends')

# Get all edges
edges = network.get_edges(data=True)

# Get neighbors of a node in a layer
neighbors = list(network.get_neighbors('Alice', layer_id='friends'))

# Get all layers
layers = network.get_layers()
print(f"Layers: {layers}")

Basic Statistics

# Get basic network statistics
network.basic_stats()

# Output:
# Number of nodes: X
# Number of edges: Y
# Number of unique nodes (as node-layer tuples): Z
# Number of unique node IDs (across all layers): W
# Nodes per layer:
#   Layer 'friends': N1 nodes
#   Layer 'colleagues': N2 nodes

Subnetwork Extraction

# Extract a single layer
layer_1 = network.subnetwork(['friends'], subset_by="layers")

# Extract specific nodes (all their appearances)
node_subset = network.subnetwork(['Alice', 'Bob'], subset_by="node_names")

# Extract specific node-layer pairs
specific_pairs = network.subnetwork(
    [('Alice', 'friends'), ('Bob', 'friends')],
    subset_by="node_layer_names"
)

Matrix Representations

# Get supra-adjacency matrix (all layers stacked)
supra_adj = network.get_supra_adjacency_matrix(sparse=True)

# Get adjacency matrix for a single layer
layer_subgraph = network.subnetwork(['friends'], subset_by="layers")
layer_adj = nx.adjacency_matrix(layer_subgraph.core_network)

Integration with NetworkX

Full NetworkX Compatibility

Since py3plex builds on NetworkX, you can use any NetworkX algorithm:

import networkx as nx
from py3plex.core import multinet

# Create py3plex network
mlnet = multinet.multi_layer_network()
mlnet.add_edges([
    ['A', 'L1', 'B', 'L1', 1],
    ['B', 'L1', 'C', 'L1', 1],
], input_type="list")

# Access underlying NetworkX graph
G = mlnet.core_network

# Use any NetworkX function
betweenness = nx.betweenness_centrality(G)
print(f"Betweenness centrality: {betweenness}")

# Shortest paths
try:
    path = nx.shortest_path(G, ('A', 'L1'), ('C', 'L1'))
    print(f"Shortest path: {path}")
except nx.NetworkXNoPath:
    print("No path exists")

# Connected components
if not G.is_directed():
    components = list(nx.connected_components(G))
    print(f"Number of components: {len(components)}")

NetworkX Wrappers

py3plex provides convenience wrappers for common NetworkX functions:

# Extract a layer
layer_1 = network.subnetwork(['layer1'], subset_by="layers")

# Apply NetworkX algorithms via wrapper
degree_centrality = layer_1.monoplex_nx_wrapper("degree_centrality")
betweenness = layer_1.monoplex_nx_wrapper("betweenness_centrality")

# Results are dictionaries mapping node-layer pairs to values
print(f"Degree centrality: {degree_centrality}")

Converting Between Formats

# From NetworkX to py3plex
import networkx as nx
from py3plex.core import multinet

G = nx.karate_club_graph()
mlnet = multinet.multi_layer_network()
# NetworkX graphs are imported as single-layer networks
mlnet.load_network_from_networkx(G)

# From py3plex to NetworkX (already a NetworkX graph!)
G_export = mlnet.core_network

# Save as standard NetworkX formats
nx.write_graphml(G_export, "output.graphml")
nx.write_gml(G_export, "output.gml")

Performance Considerations

Sparse vs. Dense Matrices

For large networks, always use sparse matrix representations:

# Good: Sparse matrix (efficient for large networks)
supra_adj = network.get_supra_adjacency_matrix(sparse=True)

# Bad: Dense matrix (memory-intensive)
# Only use for small networks (< 1000 nodes)
supra_adj_dense = network.get_supra_adjacency_matrix(sparse=False)

Memory Usage

Node-layer pairs mean that a node appearing in \(L\) layers occupies \(L\) times the memory of a single-layer network. For networks with many layers:

Tensor-Like Indexing

You can access nodes and edges using tensor-like notation:

from py3plex.core import multinet, random_generators

# Create a network
network = random_generators.random_multilayer_ER(100, 3, 0.05, directed=False)

# Get some nodes and edges
some_nodes = list(network.get_nodes())[0:5]
some_edges = list(network.get_edges())[0:5]

# Access node directly (returns node attributes)
node_data = network[some_nodes[0]]
print(f"Node {some_nodes[0]} data: {node_data}")

# Access edge (returns edge attributes)
edge_data = network[some_edges[0][0]][some_edges[0][1]]
print(f"Edge data: {edge_data}")

Design Principles

py3plex follows several key design principles:

1. NetworkX Compatibility

All operations maintain compatibility with NetworkX, allowing seamless use of the rich NetworkX ecosystem.

2. Lazy Evaluation

Expensive operations like supra-adjacency matrix construction are computed on demand and cached when appropriate.

3. Graceful Degradation

Optional features (like Infomap community detection or advanced visualizations) fail gracefully with informative error messages if dependencies are missing.

4. Extensibility

The modular design makes it easy to extend py3plex with custom algorithms and visualizations.

5. Flexibility

Multiple ways to construct and manipulate networks to suit different workflows and use cases.

Comparison with Other Representations

Why Node-Layer Pairs?

Alternative representations exist, but py3plex’s node-layer pair approach offers key advantages:

Alternative: Separate graphs per layer

Drawback: Loses inter-layer information, requires manual bookkeeping for cross-layer operations.

Alternative: Single graph with edge type attributes

Drawback: Cannot distinguish node context across layers, loses layer-specific node attributes.

py3plex approach: Node-layer pairs

Advantages:

  • Preserves both intra-layer and inter-layer structure

  • Allows layer-specific node attributes

  • Compatible with NetworkX algorithms

  • Efficient for multilayer-specific operations

Next Steps

Academic References:

  • De Domenico et al. (2013). “Mathematical formulation of multilayer networks.” Physical Review X 3(4): 041022.

  • Kivelä et al. (2014). “Multilayer networks.” Journal of Complex Networks 2(3): 203-271.