Skip to content
Snippets Groups Projects
Commit cbae4c58 authored by Aiko Voigt's avatar Aiko Voigt
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Showing with 945 additions and 0 deletions
# cctrigrid
A library for computing connected components on an unstructured triangular grid
Authors: - Aiko Voigt (University of Vienna)
- Noam Rotberg (Otto von Guericke University of Magdeburg)
- Nicole Knopf (Karlsruhe Institute of Technology)
- Petra Schwer (Otto von Guericke University of Magdeburg)
import numpy as np
import grid_functions as gridf
import cubulation_functions as cubulf
# make_cubulation
# Assign to each triangle a 3d-coordinate called cube coordinate.
# INPUT:
# start_triangle int the triangle index where the cubulatiion algorithm starts
# radius int the number of "outward" steps
# print_progress Boolean print progress of iteration (default False)
# OUTPUT:
# Returns a list of arrays of the following form:
# np.array([<triangle index>, <cube array>])
# where <cube array> is an array with three entries that encode the cube coordinate of the triangle with <triangle index>.
def make_cubulation(start_triangle, radius, print_progress=False):
# initialization
init = cubulf.EnhancedTri(start_triangle, np.array([0,0,0])) # assign coordinates (0,0,0) to start_triangle
cube_coordinates = [np.array([init.triangle, init.cube], dtype=object)]
visited_triangles = [init.triangle]
outmost = [init] # list for EnhancedTri that were added last round and thus lie "at the boundary of discovery"
# colour the edges of start_triangle with 0,1,2
edge_colours = { gridf.get_edges_of_cell(start_triangle)[x] : x for x in (0,1,2) }
# expand outwards
for n in range(radius):
# update outmost triangles
outmost = cubulf.cubing_next_round(cube_coordinates, visited_triangles, outmost, edge_colours)
# optional: show progress
if print_progress: print('Round ', n+1, 'finished. Visited ', len(cube_coordinates), \
'triangles, thereof ', len(outmost), 'new.')
# return coordinates after shifting them to positivity
return cubulf.shift_coordinates(cube_coordinates, radius)
# make_connected_2d_components:
# From given cubuluation, compute connected components and output them as a list of lists
# INPUT:
# cubulation list of arrays output of make_cubulation
# field_cube numpy array input field in cubulated 3d-coordinates
# connectivity string use 'vertex' (default) or 'edge' connectivity
# OUTPUT:
# - Returns a list of lists.
# - Each inner list corresponds to a connected component and contains
# all triangle indices that belong to this connected component
def make_connected_components_2d(cubulation, field_cube, connectivity = 'vertex'):
# compute connected components on field_cube
# components_cube is a 3-dimensional array that contains
# the connected component index of a triangle with cube coordinates (x,y,z) at position component_array[x][y][z]
components_cube = cubulf.make_connected_components(field_cube, connectivity)
# a component list of triangle indices is generated for each connected component:
# returns a list containing all component lists
components = cubulf.make_list_of_components (cubulation, components_cube)
return components
# make_connected_3d_components:
# From given cubuluation, compute connected components and output them as a list of lists
# INPUT:
# cubulation list of arrays output of make_cubulation
# field_cube numpy array input field in dimensions of (lev x cubulated coordinates)
# connectivity string use 'vertex' (default) or 'edge' connectivity
# OUTPUT:
# - Returns a list of lists.
# - Each inner list corresponds to a connected component and contains all tuples of (level, triangle index) that
# belong to this connected component
def make_connected_components_3d(cubulation, field_cube, connectivity = 'vertex'):
nlev = np.shape(field_cube)[0] # number of vertical levels
# Step 1: compute connected components for each level separately,
# in the following these are refered to as 2d connected components
components2d = []
for lev in range(nlev):
aux = make_connected_components_2d(cubulation, field_cube[lev], connectivity)
for items in aux:
components2d.append([lev]+sorted(items))
del aux
# components is now a list of sublists, each sublist has the level as its first
# entry followed by the indices of connected triangles on that level
# --> e.g., components[20] = [lev, 1058, 1059, 2099, 2100, ... ]
# Step 2: we merge the 2d connected components in the vertical --> this will yield the
# connected components in the 3-dimensional space ("the 3d connected components")
# to this end, we regard each 2d connected component as a node of an undirected graph and
# use the connected component finding for graphs
# Step 2a: we look for connections between nodes, i.e., cases when two 2d connected compoents
# are in adjacent levels and share at least on triangle with each other
# this creates a list of pairs that uses the indices components2d and are the edges of the graph
# ---> e.g., a pair might lool like pairs[0] = [0,1], in which case if would consist
# of the first and second inner list of components2d
pairs =set() # we make use of sets as it automatically removes duplicate pairs
for item1 in components2d:
for item2 in components2d:
if np.abs(item1[0]-item2[0])==1: # the two 2d connected components need to be adjacent in the vertical
# is there overlap between the two 2d connected components?
if not set(item1[1:]).isdisjoint(item2[1:]): # --> enter if there is overlap
pair = tuple(sorted([components2d.index(item1), components2d.index(item2)]))
pairs.add(pair)
# turn pairs into a sorted list
pairs = sorted(list(pairs))
# we use the implmentation of the networkx library; this choice is
# inspired by https://stackoverflow.com/a/62545221/10376696
import networkx as nx
# create ubdirected graph based on its edges, which are defined by pairs
graph = nx.Graph(pairs)
# Step 2b: we also add all 2d connected components as nodes, this is needed
# to catch cases when a 2d connected component is not connected to another 2d connected
# components in a level above or below, i.e., it does not appear in "pairs"
graph.add_nodes_from(range(0, len(components2d)))
# Step 2c: we now use connected component search on the graph to identify 2d connected components
# that belong together
# --> E.g., assume the graph consists of graph.nodes=[0, 1, 100] and
# graph.edges=[(0,1), (0,2), (1,3), (3,4), (7,8), (7,10), (10,11), (15,16), (20,24)],
# then this should result in [{100}, {0,1,2,3,4}, {7,8,10,11}, {15,16}, {20,24}].
# That is, there are five 3d connected components.
# "result" is a list of sets, which each set defining a 3d connected component
# for each set, the entries refer to the indices of components2d
result = list(nx.connected_components(graph))
del graph
# Step 3 (final step): we create the list of 3d connected components, with each
# 3d connected component being given as a sublist of (lev, triangle) tuples
components3d=list()
# loop over the sets of result
for cc3d in result:
cc3d_tuples = [] # a list of (lev, triangle) tuples that belong to this 3d connected component
# loop over the 2d connected components, or more precisely their indices in the list components
for cc2d in cc3d:
# loop over the triangles that belong to a 2d connected component,
# remember that the first entry is the level (see step 1)
lev = (components2d[cc2d])[0]
tri = (components2d[cc2d])[1:]
for ind_tri in tri:
# generate the tuples and append them to the list cc3d_tuples that define the 3d connected component
aux = tuple((lev,ind_tri))
cc3d_tuples.append(aux)
components3d.append(cc3d_tuples)
return components3d
\ No newline at end of file
ds_grid = None
\ No newline at end of file
import numpy as np
import cc3d # external library for connected component labeling on cubic grids
import grid_functions as gridf
# class to enhance a triangle with its cube coordinate
# for a given triangle create an object consisting of the triangle index and the triangle's cube coordinate
class EnhancedTri:
def __init__(self, triangle, cube):
self.triangle = triangle
self.cube = cube
def print(self):
print("Triangle index:", self.triangle, " Cube coordinate:", self.cube)
# colouring edges of a triangle
# situation: the colours of two edges are already known,
# the third edge must be coloured
# called from colour_new_edges in this special situation
# INPUT:
# first_known_edge int index of a already coloured edge
# second_known_edge int index of a already coloured edge
# new_edge int index of the not yet coloured edge
# edge_colours dict dictionary storing to each edge index the colour
# OUTPUT:
# None, but updates edge_colours
def colour_exactly_one_new_edge(first_known_edge, second_known_edge, new_edge, edge_colours):
# get the colours of the already coloured edges
first_colour = edge_colours[first_known_edge]
second_colour = edge_colours[second_known_edge]
# determine new colour, i.e. the one not taken from (0,1,2) by first_colour and second_colour
for i in (0,1,2):
if not (first_colour == i or second_colour == i):
edge_colours[new_edge] = i # colour found -> update edge_colours
return
# colouring edges of a triangle
# from old_triangle with already coloured edges determine the edge_colours of the adjacent new_triangle
# INPUT:
# old_triangle_edges set contains the edge indices of old_triangle's edges
# new_triangle_edges set contains the edge indices of new_triangle's edges
# joint_edge int index of the shared edge from old_triangle and new_triangle
# edge_colours dict dictionary storing to each edge index the colour
# OUTPUT:
# None, but updates edge_colours
def colour_new_edges(old_triangle_edges, new_triangle_edges, joint_edge, edge_colours):
# determine the edges of new_triangle that are not shared with old_triangle
new_edges = new_triangle_edges.difference( old_triangle_edges )
first_edge = new_edges.pop()
second_edge = new_edges.pop()
# are the found edges uncoloured?
first_is_coloured = first_edge in edge_colours
second_is_coloured = second_edge in edge_colours
# case: first_edge is coloured
if first_is_coloured:
if second_is_coloured:
return # both known -> nothing to colour
else:
# colour second_edge using the colours of first_edge and joint_edge
colour_exactly_one_new_edge( first_edge, joint_edge, second_edge, edge_colours )
# case: second_edge is coloured
if second_is_coloured:
# colour first_edge using the colours of second_edge and joint_edge
colour_exactly_one_new_edge( second_edge, joint_edge, first_edge, edge_colours )
# case: both edges are uncoloured
else:
# get old_triangle's edges that are not joined with new_triangle
comparison_edges = old_triangle_edges.difference( new_triangle_edges )
first_comparison_edge = comparison_edges.pop()
second_comparison_edge = comparison_edges.pop()
# get vertices of first_edge and first_comparison_edge
first_vertices = gridf.get_vertices_of_edge( first_edge )
first_comparison_vertices = gridf.get_vertices_of_edge( first_comparison_edge )
# if first_edge and first_comparison_edge have no shared vertices:
# the two edges must be parallel and thus, have the same colour
# else, first_edge must be parallel to second_comparison_edge and thus, have the same colour
parallel = set(first_vertices).isdisjoint( set(first_comparison_vertices) )
if parallel: # parallel: same colour
edge_colours[ first_edge ] = edge_colours[ first_comparison_edge ]
edge_colours[ second_edge ] = edge_colours[ second_comparison_edge ]
else: # not parallel: first_edge and second_comparison_edge are parallel and same-coloured
edge_colours[ first_edge ] = edge_colours[ second_comparison_edge ]
edge_colours[ second_edge ] = edge_colours[ first_comparison_edge ]
# compute cube coordinate of a new triangle
# given the old_triangle's cube_coordinate derive the cube coordinate of the adjacent new_triangle
# by using the colour of their joint_edge
# INPUT:
# old EnhancedTri the old EnhancedTri with known cube_coordinates
# joint_edge int index of the shared edge from old_triangle and new_triangle
# edge_colours dict dictionary storing to each edge index the colour
# OUTPUT:
# new_cube_coordinate, the cube_coordinate of new_triangle
def determine_cube_coordinates (old, old_triangle_edges, new_triangle_edges, joint_edge, edge_colours):
# get colour of the joint_edge and call it direction
direction = edge_colours[joint_edge]
# the direction encodes the parallel class of the joint edge
# this encodes also the direction in which one has to walk from old_triangle to new_triangle
# this gives the coordinate in which the cube_coordinates of old_- and new_triangle differ
# determine direction-vector (1,0,0), (0,1,0) or (0,0,1) by which the cube_coordinates will differ
direction_vector = np.array([0,0,0])
direction_vector[direction] = 1
# use invariant: sum of coordinates musst be 0 or 1
# determine wether the direction vector has to be added or subtracted
# by using the invariant that the sum of all valid cube_coordinates has to be 0 or 1
old_coordinate_sum = sum(old.cube[i] for i in (0,1,2))
if old_coordinate_sum == 0:
# old triangle's coordinate sum is 0 thus new ones has to be 1 -> add
new_cube_coordinate = old.cube + direction_vector
else:
# old triangle's coordinate sum is 1 thus new ones has to be 0 -> subtract
new_cube_coordinate = old.cube - direction_vector
return new_cube_coordinate
# iterate over the outmost triangles and compute the cube_coordinates of their adjacent triangles
# INPUT:
# cube_coordinates array EnhancedTri's of the triangles whose cube_coordinates are already computed
# visited_triangles list indices of the triangles whose cube_coordinates are already computed
# outmost list EnhancedTri's of the triangles considered in the round before
# edge_colours dict dictionary storing to each edge index the colour
# OUTPUT:
# updated outmost, EnhancedTri's of the new triangles considered this round
def cubing_next_round(cube_coordinates, visited_triangles, outmost, edge_colours):
# list for triangles that will be outmost in next iteration
new_outmost = []
for old in outmost: # consider all EnhancedTri's at the border of visited
for neigh in gridf.get_neighbors_of_cell(old.triangle ): # consider all neighbours
new = EnhancedTri(neigh, 0)
if new.triangle == -9999: # use feature: triangles at the grid border claim to be adjacent to -9999
break
else:
if new.triangle not in visited_triangles: # EnhancedTri new has no cube_coordinate yet
# add new to new_outmost for next round hereafter and update visited_triangles
new_outmost.append( new )
visited_triangles.append ( new.triangle )
# preparation: obtain edges of new_triangle and old_triangle and derive their joint_edge
old_triangle_edges = set(gridf.get_edges_of_cell(old.triangle)) # this is a set for python reasons...
new_triangle_edges = set(gridf.get_edges_of_cell(new.triangle))
joint_edge = ( new_triangle_edges & old_triangle_edges ).pop()
# colour the edges of new.triangle
colour_new_edges (old_triangle_edges, new_triangle_edges, joint_edge, edge_colours)
# get cube coordinates for EnhancedTri new and update the array of cube_coordinates
new.cube = determine_cube_coordinates (old, old_triangle_edges, new_triangle_edges, joint_edge, edge_colours)
cube_coordinates.append(np.array([new.triangle, new.cube], dtype=object))
return new_outmost
# shift cube coordinates such that they are all positive
# INPUT:
# cube_coordinates array stores triangle - cube_coordinate pairs
# radius int same as in make_cube_coordiantes
# OUTPUT:
# shifted cube_coordinates
def shift_coordinates (cube_coordinates, radius):
shift = int(radius/2)
# update all coordinates
for entry in cube_coordinates:
cube = entry[1] # get cube_coordinate
# consider all three directions
for direction in (0,1,2):
cube[direction] += shift # shift coordinate
return cube_coordinates
# compute connected components
# INPUT:
# field_cube array obtained as part of field prepaation
# connectivity string use 'vertex' (default) or 'edge' connectivity
# OUTPUT:
# component_cube, 3d array containing the component indices
def make_connected_components(field_cube, connectivity = 'vertex'):
import sys
# translate connectivity - string into input value for 3d connected component labeling
if connectivity == 'edge':
connectivity_value = 6
else:
connectivity_value = 26
if connectivity != 'vertex': # use default 'vertex' connectivity for invalid input
sys.exit("Invalid input: only 'vertex' or 'edge' connectivity are allowed.")
# calling external 3d connected component labeling
component_cube = cc3d.connected_components(field_cube, connectivity = connectivity_value)
return component_cube
# which triangle belongs to which component?
# INPUT:
# cubulation array output of make_cube_coordinates
# component_cube 3d arry output of make_connected_components
# OUTPUT:
# Returns a list of lists.
# Each inner list corresponds to one connected component and contains the indices
# of all triangles that belong to it
def make_list_of_components (cubulation, component_cube):
# number of found components on cubical grid
ncomp = component_cube.max()
# initialize list of ncomp lists, with each list holding the triangles that
# belong to a specific connected component
component_list = []
for n in range(ncomp):
component_list.append([])
# determine component
for entry in cubulation:
triangle = entry[0] # index of triangle
cube = entry[1] # cube coordinate
component = component_cube[cube[0]][cube[1]][cube[2]]
if component != 0: # triangle belongs to a connected component
# add triangle to its component list
component_list[component-1].append(triangle)
return component_list
\ No newline at end of file
# need to get access to the grid dataset
import config
ds_grid = config.ds_grid
# get_neighbors_of_cell:
# returns edge neighbors of a given triangle
def get_neighbors_of_cell(triangle):
return ds_grid.neighbor_cell_index[:,triangle].values
# get_edges_of_cell:
# returns the three edges of a given triangle
def get_edges_of_cell(triangle):
return ds_grid.edge_of_cell[:, triangle].values
return edges.values
# get_vertices_of_edge:
# returns the two vertices of a given edge
def get_vertices_of_edge(edge):
return ds_grid.edge_vertices[:, edge].values
\ No newline at end of file
import numpy as np
import xarray as xr
# path_input:
# create globale variables to open the xArray dataset from gridfile and datafile
# INPUT:
# path string location where gridfile and datafile are stored
# gridfile string name of the file containing the grid
# datafile string name of the file containing the field
# we only need the following information about the horizontal triangular grid:
# - the edge neighbors of a given triangle
# - the three edges of a given triangle
# - the two vertices of a given edge
# implementation for ICON model
def grid_icon(path, file):
ds_grid = xr.open_dataset(path + file)
ds_grid = ds_grid[['neighbor_cell_index', 'edge_of_cell', 'edge_vertices']]
# in the ICON model grid, the indexing of the triangle cells and vertices
# starts with 1 and not with 0 as assumed by cctrigrid --> we need to subtract 1 here
ds_grid['neighbor_cell_index'] = ds_grid['neighbor_cell_index'] - 1
ds_grid['edge_of_cell'] = ds_grid['edge_of_cell'] - 1
ds_grid['edge_vertices'] = ds_grid['edge_vertices'] - 1
# after the substraction of 1, triangles at the grid border claim to be adjacent to -1
# --> we set these values to -9999 to clearly flag that there is no neighboing triangle
# note: the -9999 value as a flag is used in the function "cubing_next_round"
ds_grid['neighbor_cell_index'] = xr.where(ds_grid['neighbor_cell_index']!=-1, ds_grid['neighbor_cell_index'], -9999)
return ds_grid
# prepare_field
# returns the data field as a numpy array, also applies threshold-based mask
# INPUT:
# file: file containing the data field
# var: variabe name
# threshold: set field to one if field>=threshold, and to zero otherwise
# OUTPUT:
# field both on the triangular grid as well as on the cubulated grid
# implementation for ICON model
# single-level data
def field_icon(path, file, var, threshold, cubulation):
# field on triangular grid
field = xr.open_dataset(path + file)[var].squeeze().values
field[field>=threshold] = 1
field[field< threshold] = 0
# determine size of array that holds cubulation
array_size=0
for ind in [0,1,2]:
xlist=list()
for entry in cubulation:
xlist.append(entry[1][ind])
array_size=max(array_size, max(xlist))
array_size=array_size+1 # need to add one as cubulation indices start with 1 instead of 0
# map field from triangular grid to field on cubic grid
field_cube = np.zeros((array_size, array_size, array_size), dtype = 'int')
for entry in cubulation:
triangle = entry[0] # index of triangle
cube = entry[1] # cube coordinate
field_cube[cube[0], cube[1], cube[2]] = field[triangle]
return field, field_cube
# data on multiple levels
def field_icon_lev(path, file, var, threshold, cubulation):
# field on triangular grid
field = xr.open_dataset(path + file)[var].squeeze().values
field[field>=threshold] = 1
field[field< threshold] = 0
nlev = np.shape(field)[0]
# determine size of array that holds cubulation
array_size=0
for ind in [0,1,2]:
xlist=list()
for entry in cubulation:
xlist.append(entry[1][ind])
array_size=max(array_size, max(xlist))
array_size=array_size+1 # need to add one as cubulation indices start with 1 instead of 0
# map field from triangular grid to field on nlev x cubic grid
field_cube = np.zeros((nlev, array_size, array_size, array_size), dtype = 'int')
for lev in range(nlev):
for entry in cubulation:
triangle = entry[0] # index of triangle
cube = entry[1] # cube coordinate
field_cube[lev, cube[0], cube[1], cube[2]] = field[lev, triangle]
return field, field_cube
\ No newline at end of file
File added
File added
File added
File added
File added
%% Cell type:markdown id: tags:
# Example notebook for usage with single-level data on the triangular grid
We use a 80km horizontal grid from the ICON atmosphere model with the physics package for numerical weather prediction ("ICON-NWP"). The model uses an unstructured triangular grid. The data is from a simulation over a limited-area domain over the North Atlantic in September 2016. We specifically use total cloud cover, which in ICON-NWP has values between 0 and 1.
Some more background on ICON is given, e.g., here:
* Zaengl, G. et al, 2015: The ICON (ICOsahedral Non‐hydrostatic) modelling framework of DWD and MPI‐M: Description of the non‐hydrostatic dynamical core, QJRMS. https://rmets.onlinelibrary.wiley.com/doi/abs/10.1002/qj.2378
* https://www.dwd.de/EN/research/weatherforecasting/num_modelling/01_num_weather_prediction_modells/icon_description.html
The model simulation from which the data is taken is described in Senf, F., A. Voigt et al, 2020: Increasing Resolution and Resolving Convection Improve the Simulation of Cloud‐Radiative Effects Over the North Atlantic, JGR Atmospheres. https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2020JD032667. Specifically, the simulation belongs to Set 1 (see Table 2 of Senf, Voigt et al., 2020), and used the 1-moment cloud microphysical scheme, parametrized convection, and a horizontal grid with 80 km resolution. The latter leads to 7920 triangles per level.
%% Cell type:markdown id: tags:
## Need to add cctrigrid functions to the system path
%% Cell type:code id: tags:
``` python
import sys
sys.path.append( '../cctrigrid/' )
```
%% Cell type:markdown id: tags:
## Prepare grid
First, we need to read in the triangular grid. Here, this is done for the triangular grid of the ICON model.
%% Cell type:code id: tags:
``` python
import prepare
ds_grid = prepare.grid_icon(path='./data/', file='icon-grid_nawdex_78w40e23n80n_R80000m.nc')
```
%% Cell type:markdown id: tags:
We need to make this grid available via config.py.
%% Cell type:code id: tags:
``` python
import config
config.ds_grid = ds_grid
```
%% Cell type:markdown id: tags:
## Perform cubulation
We now perform the cubulation. That is, we map the indices of the triangular grid to the indices of the cubical grid. The former are a 1d array, the latter is a list, with each list entry mapping the triangular cell index to the 3d index on the cubical grid.
Note that we also need to specify the starting triangle and the search radius. For limited-area grids, the starting triangle should usually be in the middle of the grid. The radius define the number of steps taken to enlarge the cubulated triangles "outward".
For the grid used here, start_triangle = 5738 is a good choice. As for the radius, set:
* radius = 102 to obtain the whole grid
* radius = 56 for maximal area not touching the boundaries
%% Cell type:code id: tags:
``` python
import cctrigrid
cubulation = cctrigrid.make_cubulation(start_triangle=5738, radius=102, print_progress=False)
```
%% Cell type:markdown id: tags:
For illustration, this is what cubulation looks like. The first ten entries are as follows:
%% Cell type:code id: tags:
``` python
cubulation[0:10]
```
%% Output
[array([5738, array([51, 51, 51])], dtype=object),
array([5734, array([52, 51, 51])], dtype=object),
array([5741, array([51, 52, 51])], dtype=object),
array([5735, array([51, 51, 52])], dtype=object),
array([5904, array([52, 51, 50])], dtype=object),
array([5731, array([52, 50, 51])], dtype=object),
array([5745, array([51, 52, 50])], dtype=object),
array([5739, array([50, 52, 51])], dtype=object),
array([5736, array([50, 51, 52])], dtype=object),
array([5737, array([51, 50, 52])], dtype=object)]
%% Cell type:markdown id: tags:
Because for a given grid the cubulation only needs to be done once, it might be helpful to save it for later use. We here simply use the native numpy array format.
%% Cell type:code id: tags:
``` python
import numpy as np
np.save('icon-grid_nawdex_78w40e23n80n_R80000m_cubulation_radius102_start5738', cubulation)
```
%% Cell type:markdown id: tags:
Read in ICON data for total cloud cover. Field is the data on the ICON triangular grid, field_cube on the cube grid. Also, a threhold is applied to set all values of field to zero below the threshold, and to 1 above.
%% Cell type:code id: tags:
``` python
field, field_cube = prepare.field_icon(path='./data/',
file='nawdexnwp-80km-mis-0001_2016092200_2d_30min_DOM01_ML_0060.nc',
var='clct', threshold=0.5, cubulation=cubulation)
```
%% Cell type:markdown id: tags:
## Find connected components
Now we find the connected components and write them into a list of lists. The "sublists" contain the triangle indices that belong to the same
connected components.
### We first do this for vertex connectivity.
%% Cell type:code id: tags:
``` python
components_vertex = cctrigrid.make_connected_components_2d(cubulation, field_cube, connectivity = 'vertex')
```
%% Cell type:markdown id: tags:
`components_vertex` is a list of components. It is straightforward to map it on the structure of the field input data, with the value indicating the connected component. Doing so can be handy for plotting for example. 0 means that the triangle does not belong to a connected component.
%% Cell type:code id: tags:
``` python
field_cc_vertex = np.zeros(field.size)
for icomp in range(len(components_vertex)):
field_cc_vertex[components_vertex[icomp]]=icomp+1
```
%% Cell type:markdown id: tags:
#### And now for edge connectivity.
%% Cell type:code id: tags:
``` python
components_edge = cctrigrid.make_connected_components_2d(cubulation, field_cube, connectivity = 'edge')
field_cc_edge = np.zeros(field.size)
for icomp in range(len(components_edge)):
field_cc_edge[components_edge[icomp]]=icomp+1
```
%% Cell type:markdown id: tags:
## Some sanity checks (purely optional)
We can do a few sanity checks.
%% Cell type:markdown id: tags:
1. The total number of triangles that belong to a connected component needs to be the same for vertex and edge connectivity, and needs to equal the sum of triangles for which field==1.
%% Cell type:code id: tags:
``` python
print('Total number of triangles that belong to some connected component for vertex connectivity:', np.sum([len(icomp) for icomp in components_vertex]))
print('Total number of triangles that belong to some connected component for edge connectivity:', np.sum([len(icomp) for icomp in components_edge ]))
print('Total number of triangles for which field has a value of 1:', np.sum(field==1))
```
%% Output
Total number of triangles that belong to some connected component for vertex connectivity: 4608
Total number of triangles that belong to some connected component for edge connectivity: 4608
Total number of triangles for which field has a value of 1: 4608
%% Cell type:markdown id: tags:
2. Vertex connectivity should have less connected components than edge connectivity.
%% Cell type:code id: tags:
``` python
print('Number of connected components for vertex connectivity:', len(components_vertex))
print('Number of connected components for edge connectivity:', len(components_edge ))
```
%% Output
Number of connected components for vertex connectivity: 6
Number of connected components for edge connectivity: 23
%% Cell type:markdown id: tags:
3. Let us plot the connected components for a visual inspection.
%% Cell type:markdown id: tags:
First we define a qualitative colormap that cycles through the colors of the matplotlib Set1 colormap, and that always plots 0 as white.
%% Cell type:code id: tags:
``` python
def make_colormap(ncolors):
import matplotlib as mpl
cmap_base = plt.cm.Set1 # note: number of colors of base color map is given by cmap_base.N
cmaplist_base = [cmap_base(i) for i in range(cmap_base.N)]
cmaplist = [(1, 1, 1, 1.0)] # need to have white as first color for the triangles with no connected component
for i in range(ncolors-1):
icolor = np.mod(i,cmap_base.N)
cmaplist.append(cmaplist_base[icolor])
return mpl.colors.LinearSegmentedColormap.from_list('Custom cmap', cmaplist, ncolors)
```
%% Cell type:markdown id: tags:
Now we plot the thresholded input field, as well as the connected components for vertex and edge connectivity. For vertex connectivity, triangles touching at a shared vertex belong to the same component. For edge connectivity, only triangles that touch via a shared edge belong to the same connected component. Thus, edge connectivity is stricter and leads to more connected components. This plots below confirm this expectation.
%% Cell type:code id: tags:
``` python
import matplotlib.pyplot as plt
import matplotlib.tri as tri
rad2deg=180.0/np.pi
import xarray as xr
ds_grid_plot = xr.load_dataset('./data/icon-grid_nawdex_78w40e23n80n_R80000m.nc')
vlat = rad2deg*ds_grid_plot['vlat'].values
vlon = rad2deg*ds_grid_plot['vlon'].values
# we need to subtract -1 from vertex_of_cell as python starts counting at 0, but fortran starts at 1
vertex_of_cell= ds_grid_plot['vertex_of_cell'].values-1
del ds_grid_plot
# plotting
plt.figure(figsize=(20,40))
plt.subplot(3,1,1)
plt.title('thresholded input field', fontsize=14)
c=plt.tripcolor(vlon, vlat, vertex_of_cell.transpose(), facecolors=field, vmin=-0.5, vmax=1.5, edgecolors='none', cmap=make_colormap(ncolors=2))
plt.colorbar(c, ticks=[0,1]);
plt.xlim(-80,40); plt.ylim(23,80);
plt.xlabel('longitude', fontsize=14); plt.ylabel('latitude', fontsize=14);
plt.subplot(3,1,2)
plt.title('vertex connectivity', fontsize=14)
c=plt.tripcolor(vlon, vlat, vertex_of_cell.transpose(), facecolors=field_cc_vertex, vmin=-0.5,
vmax=len(components_vertex)+0.5, edgecolors='none', cmap=make_colormap(ncolors=len(components_vertex)+1))
plt.colorbar(c, ticks=[i for i in range(0,len(components_vertex)+1)]);
plt.xlim(-80,40); plt.ylim(23,80);
plt.xlabel('longitude', fontsize=14); plt.ylabel('latitude', fontsize=14);
plt.subplot(3,1,3)
plt.title('edge connectivity', fontsize=14)
c=plt.tripcolor(vlon, vlat, vertex_of_cell.transpose(), facecolors=field_cc_edge, vmin=-0.5,
vmax=len(components_edge)+0.5, edgecolors='none', cmap=make_colormap(ncolors=len(components_edge)+1))
plt.colorbar(c, ticks=[i for i in range(0,len(components_edge)+1)]);
plt.xlim(-80,40); plt.ylim(23,80);
plt.xlabel('longitude', fontsize=14); plt.ylabel('latitude', fontsize=14);
plt.savefig('example-2d_plot.pdf')
```
%% Output
Source diff could not be displayed: it is too large. Options to address this: view the blob.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment