Source code for pyunicorn.climate.coupled_climate_network

# This file is part of pyunicorn.
# Copyright (C) 2008--2024 Jonathan F. Donges and pyunicorn authors
# URL: <https://www.pik-potsdam.de/members/donges/software-2/software>
# License: BSD (3-clause)
#
# Please acknowledge and cite the use of this software and its authors
# when results are used in publications or published elsewhere.
#
# You can use the following reference:
# J.F. Donges, J. Heitzig, B. Beronov, M. Wiedermann, J. Runge, Q.-Y. Feng,
# L. Tupikina, V. Stolbova, R.V. Donner, N. Marwan, H.A. Dijkstra,
# and J. Kurths, "Unified functional network and nonlinear time series analysis
# for complex systems science: The pyunicorn package"

"""
Provides classes for generating and analyzing complex coupled climate networks.
"""

import numpy as np

from ..core import InteractingNetworks, GeoNetwork, GeoGrid
from .climate_network import ClimateNetwork


[docs] class CoupledClimateNetwork(InteractingNetworks, ClimateNetwork): """ Encapsulates a coupled similarity network embedded on a spherical surface. Particularly provides functionality to generate a complex network from the matrix of a similarity measure of time series from two different observables (temperature, pressure), vertical levels etc. So far, most methods only give meaningful results for undirected networks! The idea of coupled climate networks is based on the concept of coupled patterns, for a review refer to [Bretherton1992]_. .. note:: The two observables (layers) need to have the same time grid \ (temporal sampling points). """ # # Definitions of internal methods #
[docs] def __init__(self, grid_1, grid_2, similarity_measure, threshold=None, link_density=None, non_local=False, directed=False, node_weight_type="surface", silence_level=0): """ Initialize an instance of CoupledClimateNetwork. .. note:: Either threshold **OR** link_density have to be given! Possible choices for ``node_weight_type``: - None (constant unit weights) - "surface" (cos lat) - "irrigation" (cos**2 lat) :type grid_1: :class:`.GeoGrid` :arg grid_1: The GeoGrid object describing the first layer's spatial embedding. :type grid_2: :class:`.GeoGrid` :arg grid_2: The GeoGrid object describing the second layer's spatial embedding. :type similarity_measure: 2D array [index, index] :arg similarity_measure: The similarity measure for all pairs of nodes. :arg float threshold: The threshold of similarity measure, above which two nodes are linked in the network. :arg float link_density: The networks's desired link density. :arg bool non_local: Determines, whether links between spatially close nodes should be suppressed. :arg bool directed: Determines, whether the network is treated as directed. :arg strnode_weight_type: The type of geographical node weight to be used. :arg int silence_level: The inverse level of verbosity of the object. """ # Store single grids self.grid_1 = grid_1 """(Grid) - The GeoGrid object describing the first layer's spatial embedding.""" self.grid_2 = grid_2 """(Grid) - The GeoGrid object describing the second layer's spatial embedding.""" # Construct grid object describing both layers time_1 = grid_1.grid()["time"] lat_1 = grid_1.grid()["lat"] lon_1 = grid_1.grid()["lon"] time_2 = grid_2.grid()["time"] lat_2 = grid_2.grid()["lat"] lon_2 = grid_2.grid()["lon"] if len(time_1) == len(time_2): grid = GeoGrid(time_1, np.concatenate((lat_1, lat_2)), np.concatenate((lon_1, lon_2))) # Set total number of nodes self.N = grid.N """(number (int)) - The total number of nodes in both layers.""" # Set number of nodes for both layers self.N_1 = len(lat_1) """(number (int)) - The number of nodes in the first layer.""" self.N_2 = len(lat_2) """(number (int)) - The number of nodes in the second layer.""" # Create lists of node indices for both layers self.nodes_1 = list(range(self.N_1)) """(list (int)) - List of node indices for first layer""" self.nodes_2 = list(range(self.N_1, self.N)) """(list (int)) - List of node indices for second layer""" # Call the constructor of the parent class ClimateNetwork ClimateNetwork.__init__(self, grid=grid, similarity_measure=similarity_measure, threshold=threshold, link_density=link_density, non_local=non_local, directed=directed, node_weight_type=node_weight_type, silence_level=silence_level) InteractingNetworks.__init__(self, self.adjacency) else: print("The two observables (layers) have to have the same number " "of temporal sampling points!")
[docs] def __str__(self): """ Return a string representation of CoupledClimateNetwork object. """ return (f'CoupledClimateNetwork:\n{ClimateNetwork.__str__(self)}\n' f'N1: {self.N_1}\nN2: self.N_2')
# # Define methods for handling the coupled network #
[docs] def network_1(self): """ Return network consisting of layer 1 nodes and their internal links. This can be used to conveniently analyze the layer 1 separately, e.g., for calculation network measures solely for layer 1. :rtype: GeoNetwork :return: the network consisting of layer 1 nodes and their internal links. """ return GeoNetwork(adjacency=self.adjacency_1(), grid=self.grid_1, directed=self.directed, node_weight_type=self.node_weight_type, silence_level=self.silence_level)
[docs] def network_2(self): """ Return network consisting of layer 2 nodes and their internal links. This can be used to conveniently analyze the layer 2 separately, e.g., for calculation network measures solely for layer 2. :rtype: GeoNetwork :return: the network consisting of layer 2 nodes and their internal links. """ return GeoNetwork(adjacency=self.adjacency_2(), grid=self.grid_2, directed=self.directed, node_weight_type=self.node_weight_type, silence_level=self.silence_level)
[docs] def similarity_measure_1(self): """ Return internal similarity measure matrix of first layer. :rtype: 2D Numpy array [index_1, index_1] :return: the internal similarity measure matrix of first layer. """ return self.similarity_measure()[:self.N_1, :self.N_1]
[docs] def similarity_measure_2(self): """ Return internal similarity measure matrix of second layer. :rtype: 2D Numpy array [index_2, index_2] :return: the internal similarity measure matrix of first layer. """ return self.similarity_measure()[self.N_1:, self.N_1:]
[docs] def cross_similarity_measure(self): """ Return cross similarity measure matrix. .. note:: Cross similarity measure matrix is NEITHER square NOR \ symmetric in general! :rtype: 2D Numpy array [index_1, index_2] :return: the cross similarity measure matrix. """ return self.similarity_measure()[:self.N_1, self.N_1:]
[docs] def adjacency_1(self): """ Return internal adjacency matrix of first layer. :rtype: 2D Numpy array [index_1, index_1] :return: the internal adjacency matrix of first layer. """ return self.internal_adjacency(self.nodes_1)
[docs] def adjacency_2(self): """ Return internal adjacency matrix of second layer. :rtype: 2D Numpy array [index_2, index_2] :return: the internal adjacency matrix of second layer. """ return self.internal_adjacency(self.nodes_2)
[docs] def cross_layer_adjacency(self): """ Return cross adjacency matrix of the coupled network. The cross adjacency matrix entry :math:`CA_{ij} = 1` describes that node :math:`i` in the first layer is linked to node :math:`j` in the second layer. Vice versa, :math:`CA_{ji} = 1` indicates that node :math:`j` in the first layer is linked to node :math:`i` in the second layer. .. note:: Cross adjacency matrix is **NEITHER** square **NOR** symmetric in general! :rtype: 2D Numpy array [index_1, index_2] :return: the cross adjacency matrix. """ return self.cross_adjacency(node_list1=self.nodes_1, node_list2=self.nodes_2)
[docs] def path_lengths_1(self, link_attribute=None): """ Return internal path length matrix of first layer. Contains the paths length between all pairs of nodes within layer 1. However, the paths themselves will generally contain nodes from both layers. To avoid this and only consider paths lying within layer 1, do the following:: net_1 = coupled_network.network_1() path_lengths_1 = net_1.path_lengths(link_attribute) :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: 2D array [index_1, index_1] :return: the internal path length matrix of first layer. """ return self.internal_path_lengths(node_list=self.nodes_1, link_attribute=link_attribute)
[docs] def path_lengths_2(self, link_attribute=None): """ Return internal path length matrix of second layer. Contains the path lengths between all pairs of nodes within layer 2. However, the paths themselves will generally contain nodes from both layers. To avoid this and only consider paths lying within layer 2, do the following:: net_2 = coupled_network.network_2() path_lengths_2 = net_2.path_lengths(link_attribute) :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: 2D array [index_2, index_2] :return: the internal path length matrix of second layer. """ return self.internal_path_lengths(node_list=self.nodes_2, link_attribute=link_attribute)
[docs] def cross_path_lengths(self, link_attribute=None): """ Return cross path length matrix. Contains the path length between nodes from different layers. The paths contain nodes from both layers. :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: 2D array [index_1, index_2] :return: the cross path length matrix. """ return InteractingNetworks.\ cross_path_lengths(self, node_list1=self.nodes_1, node_list2=self.nodes_2, link_attribute=link_attribute)
# # Define scalar coupled network statistics #
[docs] def internal_global_clustering(self): """ Return global clustering coefficients for each layer separately. Internal global clustering coefficients are calculated as mean values from the local clustering sequence of the whole coupled network. This implies that triangles spanning both layers will generally contribute to the internal clustering coefficients. To avoid this and consider only triangles lying within each layer:: net_1 = coupled_network.network_1() clustering_1 = net_1.global_clustering() net_2 = coupled_network.network_2() clustering_2 = net_2.global_clustering() :rtype: (float, float) :return: the internal global clustering coefficients. """ clustering_1 = InteractingNetworks.\ internal_global_clustering(self, self.nodes_1) clustering_2 = InteractingNetworks.\ internal_global_clustering(self, self.nodes_2) return (clustering_1, clustering_2)
[docs] def cross_global_clustering(self): """ Return global cross clustering for coupled network. The global cross clustering coefficient C_v gives the average probability, that two randomly drawn neighbors in layer 2 of node v in layer 1 are also neighbors and vice versa. It counts triangles having one vertex in layer 1 and two vertices in layer 2 and vice versa. :rtype: (float, float) :return: the cross global clustering coefficients. """ cc_12 = InteractingNetworks.cross_global_clustering( self, node_list1=self.nodes_1, node_list2=self.nodes_2) cc_21 = InteractingNetworks.cross_global_clustering( self, node_list1=self.nodes_2, node_list2=self.nodes_1) return (cc_12, cc_21)
[docs] def cross_transitivity(self): """ Return cross transitivity for coupled network. The cross transitivity is the probability, that two randomly drawn neighbors in layer 2 of node v in layer 1 are also neighbors and vice versa. It counts triangles having one vertex in layer 1 and two vertices in layer 2 and vice versa. Cross transitivity tends to weight low cross degree vertices less strongly when compared to the global cross clustering coefficient (see [Newman2003]_). :rtype: (float, float) :return: the cross transitivities. """ ct_12 = InteractingNetworks.cross_transitivity( self, node_list1=self.nodes_1, node_list2=self.nodes_2) ct_21 = InteractingNetworks.cross_transitivity( self, node_list1=self.nodes_2, node_list2=self.nodes_1) return (ct_12, ct_21)
[docs] def cross_average_path_length(self, link_attribute=None): """ Return cross average path length. Return the average (weighted) shortest path length between all pairs of nodes from different layers only. :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :return float: the cross average path length. """ return InteractingNetworks.cross_average_path_length( self, node_list1=self.nodes_1, node_list2=self.nodes_2, link_attribute=link_attribute)
[docs] def internal_average_path_length(self, link_attribute=None): """ Return internal average path length. Return the average (weighted) shortest path length between all pairs of nodes within each layer separately for which a path exists. Paths between nodes from different layers are not included in the averages! However, even if the end points lie within the same layer, the paths themselves will generally contain nodes from both layers. To avoid this and only consider paths lying within layer i, do the following:: net_i = coupled_network.network_i() path_lengths_i = net_i.path_lengths(link_attribute) :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: (float, float) :return: the internal average path length. """ apl_1 = InteractingNetworks.internal_average_path_length( self, node_list=self.nodes_1, link_attribute=link_attribute) apl_2 = InteractingNetworks.internal_average_path_length( self, node_list=self.nodes_2, link_attribute=link_attribute) return (apl_1, apl_2)
# # Define local coupled network measures #
[docs] def cross_degree(self): """ Return the cross degree sequences. Gives the number of links from a specific node in one layer to the other layer. :rtype: tuple of two 1D arrays [index] :return: the cross degree sequences. """ cross_degree_1 = InteractingNetworks.cross_degree( self, node_list1=self.nodes_1, node_list2=self.nodes_2) cross_degree_2 = InteractingNetworks.cross_degree( self, node_list1=self.nodes_2, node_list2=self.nodes_1) return (cross_degree_1, cross_degree_2)
[docs] def internal_degree(self): """ Return the internal degree sequences. Gives the number of links from a specific node to other nodes within the same layer. :rtype: tuple of two 1D arrays [index] :return: the internal degree sequences. """ degree_1 = InteractingNetworks.internal_degree( self, node_list=self.nodes_1) degree_2 = InteractingNetworks.internal_degree( self, node_list=self.nodes_2) return (degree_1, degree_2)
[docs] def cross_local_clustering(self): """ Return local cross clustering for coupled network. The local cross clustering coefficient C_v gives the probability, that two randomly drawn neighbors in layer 2 of node v in layer 1 are also neighbors and vice versa. It counts triangles having one vertex in layer 1 and two vertices in layer 2 and vice versa. :rtype: tuple of two 1D arrays [index] :return: the cross local clustering coefficients. """ cc_12 = InteractingNetworks.cross_local_clustering( self, node_list1=self.nodes_1, node_list2=self.nodes_2) cc_21 = InteractingNetworks.cross_local_clustering( self, node_list1=self.nodes_2, node_list2=self.nodes_1) return (cc_12, cc_21)
[docs] def cross_closeness(self, link_attribute=None): """ Return cross closeness sequence. Gives the inverse average geodesic distance from a node in one layer to all nodes in the other layer. :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: tuple of two 1D arrays [index] :return: the cross closeness sequence. """ cc_12 = InteractingNetworks.cross_closeness( self, node_list1=self.nodes_1, node_list2=self.nodes_2, link_attribute=link_attribute) cc_21 = InteractingNetworks.cross_closeness( self, node_list1=self.nodes_2, node_list2=self.nodes_1, link_attribute=link_attribute) return (cc_12, cc_21)
[docs] def internal_closeness(self, link_attribute=None): """ Return internal closeness sequence. Gives the inverse average geodesic distance from a node to all other nodes in the same layer. However, the included paths will generally contain nodes from both layers. To avoid this, do the following:: net_i = coupled_network.network_i() closeness_i = net_i.closeness(link_attribute) :arg str link_attribute: Optional name of the link attribute to be used as the links' length. If None, links have length 1. (Default: None) :rtype: tuple of two 1D arrays [index] :return: the internal closeness sequence. """ closeness_1 = InteractingNetworks.internal_closeness( self, node_list=self.nodes_1, link_attribute=link_attribute) closeness_2 = InteractingNetworks.internal_closeness( self, node_list=self.nodes_2, link_attribute=link_attribute) return (closeness_1, closeness_2)
[docs] def cross_betweenness(self): """ Return the cross betweenness sequence. Gives the normalized number of shortest paths only between nodes from **different** layers, in which a node :math:`i` is contained. This is equivalent to the inter-regional / inter-group betweenness with respect to layer 1 and layer 2. :rtype: tuple of two 1D arrays [index] :return: the cross betweenness sequence. """ cb = InteractingNetworks.cross_betweenness( self, node_list1=self.nodes_1, node_list2=self.nodes_2) return (cb[self.nodes_1], cb[self.nodes_2])
[docs] def internal_betweenness_1(self): """ Return the internal betweenness sequences for layer 1. Gives the normalized number of shortest paths only between nodes from layer 1, in which a node :math:`i` is contained. :math:`i` can be member of any of the two layers. This is equivalent to the inter-regional / inter-group betweenness with respect to layer 1 and layer 1. :rtype: tuple of two 1D arrays [index] :return: the internal betweenness sequence for layer 1. """ ib = self.internal_betweenness(self.nodes_1) return (ib[self.nodes_1], ib[self.nodes_2])
[docs] def internal_betweenness_2(self): """ Return the internal betweenness sequences for layer 2. Gives the normalized number of shortest paths only between nodes from layer 2, in which a node :math:`i` is contained. :math:`i` can be member of any of the two layers. This is equivalent to the inter-regional / inter-group betweenness with respect to layer 2 and layer 2. :rtype: tuple of two 1D arrays [index] :return: the internal betweenness sequence for layer 2. """ ib = self.internal_betweenness(self.nodes_2) return (ib[self.nodes_1], ib[self.nodes_2])