Source code for pyunicorn.core.grid

# 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 class for spatio-temporal grids.
"""

from typing import Tuple
from collections.abc import Hashable
import pickle

import numpy as np

from .cache import Cached
from ._ext.types import to_cy, FIELD
from ._ext.numerics import _calculate_euclidean_distance


[docs] class Grid(Cached): """ Encapsulates a spatio-temporal grid. The spatial grid points can be arbitrarily distributed, which is useful for representing station data or geodesic grids. """ # # Definitions of internal methods #
[docs] def __init__(self, time_seq: np.ndarray, space_seq: np.ndarray, silence_level: int = 0): """ Initialize an instance of Grid. :type time_seq: 1D Numpy array [time] :arg time_seq: The increasing sequence of temporal sampling points. :type space_seq: 2D Numpy array [dim, index] :arg space_seq: The sequences of spatial sampling points. :type silence_level: number (int) :arg silence_level: The inverse level of verbosity of the object. """ # Set basic dictionaries self._grid = {"time": time_seq.astype("float32"), "space": space_seq.astype("float32")} self._grid_size = {"time": len(time_seq), "space": space_seq.shape[1]} self._boundaries = {"time_min": time_seq.min(), "time_max": time_seq.max(), "space_min": np.amin(space_seq, axis=1), "space_max": np.amax(space_seq, axis=1)} # Defines the number of spatial grid points / nodes at one instant # of time self.N = self._grid_size["space"] """(number (int)) - The number of spatial grid points / nodes.""" # Set silence level self.silence_level = silence_level """(number (int)) - The inverse level of verbosity of the object.""" # Defines the total number of data points / grid points / samples of # the corresponding data set. self.n_grid_points = self._grid_size["time"] * self.N """(number (int)) - The total number of data points / samples."""
[docs] def __cache_state__(self) -> Tuple[Hashable, ...]: # The following attributes are assumed immutable: # (N, _grid) return ()
[docs] def __str__(self): """ Return a string representation of the Grid object. """ return (f"Grid: {self._grid_size['space']} grid points, " f"{self._grid_size['time']} timesteps.")
# # Functions for loading and saving the Grid object #
[docs] def save(self, filename): """ Save the Grid object to a pickle file. :arg str filename: The name of the file where Grid object is stored (including ending). """ try: with open(filename, 'wb') as f: pickle.dump(self, f) except IOError: print("An error occurred while saving Grid instance to " f"pickle file {filename}")
[docs] @staticmethod def Load(filename): """ Return a Grid object stored in a pickle file. :arg str filename: The name of the file where Grid object is stored (including ending). :rtype: Grid object :return: :class:`Grid` instance. """ with open(filename, 'rb') as f: grid = pickle.load(f) return grid
# # Alternative constructors and Grid generation methods #
[docs] @staticmethod def SmallTestGrid(): """ Return test grid of 6 spatial grid points with 10 temporal sampling points each. :rtype: Grid instance :return: a Grid instance for testing purposes. """ return Grid(time_seq=np.arange(10), space_seq=np.array([[0, 5, 10, 15, 20, 25], [2.5, 5., 7.5, 10., 12.5, 15.]]), silence_level=2)
[docs] @staticmethod def RegularGrid(time_seq, space_grid, silence_level=0): """ Initialize an instance of a regular grid. **Examples:** >>> Grid.RegularGrid( ... time_seq=np.arange(2), space_grid=[np.array([0.,5.]), np.array([1.,2.])], silence_level=2).sequence(0) array([ 0., 0., 5., 5.], dtype=float32) >>> Grid.RegularGrid( ... time_seq=np.arange(2), space_grid=[np.array([0.,5.]), np.array([1.,2.])], silence_level=2).sequence(1) array([ 1., 2., 1., 2.], dtype=float32) :type time_seq: 1D Numpy array [time] :arg time_seq: The increasing sequence of temporal sampling points. :type space_grids: list of 1D Numpy arrays [dim, n] :arg space_grids: The spatial grid. :type silence_level: number (int) :arg silence_level: The inverse level of verbosity of the object. :rtype: Grid object :return: :class:`Grid` instance. """ # Generate sequences of positions for all nodes space_seq = Grid.coord_sequence_from_rect_grid(space_grid) # Return instance of Grid return Grid(time_seq, space_seq, silence_level)
# # Definitions of grid related functions #
[docs] @staticmethod def coord_sequence_from_rect_grid(space_grid): """ Return the sequences of coordinates for a regular and rectangular grid. **Example:** >>> Grid.coord_sequence_from_rect_grid( ... space_grid=[np.array([0.,5.]), np.array([1.,2.])] [array([ 0., 0., 5., 5.]), array([ 1., 2., 1., 2.])] :type space_grid: list of 1D Numpy arrays [dim, n] :arg space_grid: The grid's sampling points. :rtype: list of 1D Numpy arrays [index] :return: the coordinates of all nodes in the grid. """ space_seq = np.meshgrid(*space_grid) space_seq = np.array([dim_seq.flatten('F') for dim_seq in space_seq]) return space_seq
[docs] def sequence(self, dimension): """ Return the positional sequence for all nodes for the specified dimension. **Example:** >>> Grid.SmallTestGrid().sequence(0) array([ 0., 5., 10., 15., 20., 25.], dtype=float32) :type dimension: integer :arg dimension: The number of the dimension :rtype: 1D Numpy array [index] :return: the sequence of positions in the specified dimension for all nodes. """ return self._grid["space"][dimension]
[docs] def node_number(self, x): """ Return the index of the closest node given euclidean coordinates. **Example:** >>> Grid.SmallTestGrid().node_number(x=(14., 9.)) 3 :type x: number (float) :arg x: The x coordinate. :type y: number (float) :arg y: The y coordinate. :rtype: number (int) :return: the closest node's index. """ x = np.array(x) # Get the differences of the coordinates of all nodes to the given # coordinates diff = self._grid["space"].T - x # Get sequences of cosLat, sinLat, cosLon and sinLon for all nodes dist = np.sqrt(np.sum(diff**2, axis=1)) # Get index of closest node n_node = dist.argmin() return n_node
[docs] def node_coordinates(self, index): """ Return the position of node ``index``. **Example:** >>> Grid.SmallTestGrid().node_coordinates(3) [15.0, 10.0] :type index: number (int) :arg index: The node index as used in node sequences. :rtype: tuple of number (float) :return: the node's coordinates. """ return tuple(self._grid["space"][:, index])
[docs] def distance(self): """ Calculate and return the standard distance matrix of the corresponding grid type :rtype: 2D Numpy array [index, index] :return: the distance matrix. """ return self.euclidean_distance()
[docs] @Cached.method() def euclidean_distance(self): """ Return the euclidean distance matrix between grid points. **Example:** >>> Grid.SmallTestGrid().euclidean_distance().round(2) [[ 0. 5.59 11.18 16.77 22.36 27.95] [ 5.59 0. 5.59 11.18 16.77 22.36] [11.18 5.59 0. 5.59 11.18 16.77] [16.77 11.18 5.59 0. 5.59 11.18] [22.36 16.77 11.18 5.59 0. 5.59] [27.95 22.36 16.77 11.18 5.59 0. ]] :rtype: 2D Numpy array [index, index] :return: the euclidean distance matrix. """ # Get number of nodes N_nodes = self.N # Get sequences of coordinates sequences = to_cy(self._grid["space"], FIELD) # Get number of dimensions N_dim = sequences.shape[0] distance = np.zeros((N_nodes, N_nodes), dtype=FIELD) _calculate_euclidean_distance(sequences, distance, N_dim, N_nodes) return distance
[docs] def boundaries(self): """ Return the spatio-temporal grid boundaries. Structure of the returned dictionary: - self._boundaries = {"time_min": time_seq.min(), "time_max": time_seq.max(), "space_min": np.amax(space_seq, axis=1), "space_max": np.amin(space_seq, axis=1)} :rtype: dictionary :return: the spatio-temporal grid boundaries. """ return self._boundaries
[docs] def grid(self): """ Return the grid's spatio-temporal sampling points. Structure of the returned dictionary: - self._grid = {"time": time_seq.astype("float32"), "space": space_seq.astype("float32")} **Examples:** >>> Grid.SmallTestGrid().grid()["space"][0] array([ 0., 5., 10., 15., 20., 25.], dtype=float32) >>> Grid.SmallTestGrid().grid()["space"][0][5] 15.0 :rtype: dictionary :return: the grid's spatio-temporal sampling points. """ return self._grid
[docs] def grid_size(self): """ Return the sizes of the grid's spatial and temporal dimensions. Structure of the returned dictionary: - self._grid_size = {"time": len(time_seq), "space": space_seq.shape[1]} **Example:** >>> print(Grid2D.SmallTestGrid().print_grid_size()) space time 6 10 :rtype: dictionary :return: the sizes of the grid's spatial and temporal dimensions. """ return self._grid_size
[docs] def print_grid_size(self): """ Pretty print the sizes of the grid's spatial and temporal dimensions. """ return " space time\n {space:7} {time:7}".format( **self.grid_size())
[docs] def geometric_distance_distribution(self, n_bins): """ Return the distribution of distances between all pairs of grid points. **Examples:** >>> Grid.SmallTestGrid().geometric_distance_distribution(3)[0].round(2) array([0.33, 0.47, 0.2 ]) >>> Grid.SmallTestGrid().geometric_distance_distribution(3)[1].round(2) array([ 0. , 9.32, 18.63, 27.95], dtype=float32) :type n_bins: number (int) :arg n_bins: The number of histogram bins. :rtype: tuple of two 1D Numpy arrays [bin] :return: the normalized histogram and lower bin boundaries of distances. """ if self.silence_level <= 1: print("Calculating the geometric distance distribution of the " "grid...") # Get angular distance matrix D = self.distance() # Determine range for link distance histograms max_range = D.max() interval = (0, max_range) # Calculate geometry related factor of distributions to divide it out (dist, lbb) = np.histogram(a=D, bins=n_bins, range=interval) # Subtract self.N from first bin because of spurious links with zero # distance on the diagonal of the angular distance matrix dist[0] -= self.N # Normalize distribution dist = dist.astype("float") dist /= dist.sum() return (dist, lbb)