Source code for pycmor.fesom_1p4.load_mesh_data

# This file is part of pyfesom
#
################################################################################
#
# Original code by Dmitry Sidorenko, 2013
# https://github.com/FESOM/pyfesom2
#
# Modifications:
#   Nikolay Koldunov, 2016
#          - optimisation of reading ASCII fies (add pandas dependency)
#          - move loading and processing of the mesh to the mesh class itself
#
#   Paul Gierz, 2024
#          - extracted relevant functions from original code only for pymor
#          - general cleanup of booleans (usepickle, usejoblib)
#
################################################################################

import logging
import math as mt
import os
import pickle
from datetime import datetime

import joblib
import numpy as np
import pandas as pd


[docs] def scalar_r2g(al, be, ga, rlon, rlat): """ Converts rotated coordinates to geographical coordinates. Parameters ---------- al : float alpha Euler angle be : float beta Euler angle ga : float gamma Euler angle rlon : array 1d array of longitudes in rotated coordinates rlat : array 1d araay of latitudes in rotated coordinates Returns ------- lon : array 1d array of longitudes in geographical coordinates lat : array 1d array of latitudes in geographical coordinates """ rad = mt.pi / 180 al = al * rad be = be * rad ga = ga * rad rotate_matrix = np.zeros(shape=(3, 3)) rotate_matrix[0, 0] = np.cos(ga) * np.cos(al) - np.sin(ga) * np.cos(be) * np.sin(al) rotate_matrix[0, 1] = np.cos(ga) * np.sin(al) + np.sin(ga) * np.cos(be) * np.cos(al) rotate_matrix[0, 2] = np.sin(ga) * np.sin(be) rotate_matrix[1, 0] = -np.sin(ga) * np.cos(al) - np.cos(ga) * np.cos(be) * np.sin( al ) rotate_matrix[1, 1] = -np.sin(ga) * np.sin(al) + np.cos(ga) * np.cos(be) * np.cos( al ) rotate_matrix[1, 2] = np.cos(ga) * np.sin(be) rotate_matrix[2, 0] = np.sin(be) * np.sin(al) rotate_matrix[2, 1] = -np.sin(be) * np.cos(al) rotate_matrix[2, 2] = np.cos(be) rotate_matrix = np.linalg.pinv(rotate_matrix) rlat = rlat * rad rlon = rlon * rad # Rotated Cartesian coordinates: xr = np.cos(rlat) * np.cos(rlon) yr = np.cos(rlat) * np.sin(rlon) zr = np.sin(rlat) # Geographical Cartesian coordinates: xg = rotate_matrix[0, 0] * xr + rotate_matrix[0, 1] * yr + rotate_matrix[0, 2] * zr yg = rotate_matrix[1, 0] * xr + rotate_matrix[1, 1] * yr + rotate_matrix[1, 2] * zr zg = rotate_matrix[2, 0] * xr + rotate_matrix[2, 1] * yr + rotate_matrix[2, 2] * zr # Geographical coordinates: lat = np.arcsin(zg) lon = np.arctan2(yg, xg) a = np.where((np.abs(xg) + np.abs(yg)) == 0) if a: lon[a] = 0 lat = lat / rad lon = lon / rad return (lon, lat)
[docs] def load_mesh(path, abg=[50, 15, -90], get3d=True, usepickle=True, usejoblib=False): """Loads FESOM mesh Parameters ---------- path : str Path to the directory with mesh files abg : list alpha, beta and gamma Euler angles. Default [50, 15, -90] get3d : bool do we load complete 3d mesh or only 2d nodes. Returns ------- mesh : object fesom_mesh object """ python_version = "3" path = os.path.abspath(path) if usepickle and usejoblib: raise ValueError( "Both `usepickle` and `usejoblib` set to True, select only one" ) if usepickle: pickle_file = os.path.join(path, "pickle_mesh_py3") if usejoblib: joblib_file = os.path.join(path, "joblib_mesh") if usepickle and (os.path.isfile(pickle_file)): print("The usepickle == True)") print("The pickle file for python 3 exists.") print("The mesh will be loaded from {}".format(pickle_file)) ifile = open(pickle_file, "rb") mesh = pickle.load(ifile) ifile.close() return mesh elif usepickle and not os.path.isfile(pickle_file): print("The usepickle == True") print("The pickle file for python 3 DO NOT exists") print("The mesh will be saved to {}".format(pickle_file)) mesh = fesom_mesh(path=path, abg=abg, get3d=get3d) try: logging.info("Use pickle to save the mesh information") print("Save mesh to binary format") outfile = open(pickle_file, "wb") pickle.dump(mesh, outfile) outfile.close() except OSError: logging.warning("Unable to save pickle as mesh cache, sorry...") return mesh elif not usepickle and not usejoblib: mesh = fesom_mesh(path=path, abg=abg, get3d=get3d) return mesh if usejoblib and os.path.isfile(joblib_file): print("The usejoblib == True)") print("The joblib file for python {} exists.".format(str(python_version))) print("The mesh will be loaded from {}".format(joblib_file)) mesh = joblib.load(joblib_file) return mesh elif usejoblib and not os.path.isfile(joblib_file): print("The usejoblib == True") print("The joblib file for python {} DO NOT exists".format(str(python_version))) print("The mesh will be saved to {}".format(joblib_file)) mesh = fesom_mesh(path=path, abg=abg, get3d=get3d) logging.info("Use joblib to save the mesh information") print("Save mesh to binary format") joblib.dump(mesh, joblib_file) return mesh
[docs] class fesom_mesh(object): """Creates instance of the FESOM mesh. This class creates instance that contain information about FESOM mesh. At present the class works with ASCII representation of the FESOM grid, but should be extended to be able to read also netCDF version (probably UGRID convention). Minimum requirement is to provide the path to the directory, where following files should be located (not nessesarely all of them will be used): - nod2d.out - nod3d.out - elem2d.out - aux3d.out Parameters ---------- path : str Path to the directory with mesh files abg : list alpha, beta and gamma Euler angles. Default [50, 15, -90] get3d : bool do we load complete 3d mesh or only 2d nodes. Attributes ---------- path : str Path to the directory with mesh files x2 : array x position (lon) of the surface node y2 : array y position (lat) of the surface node n2d : int number of 2d nodes e2d : int number of 2d elements (triangles) n3d : int number of 3d nodes nlev : int number of vertical levels zlevs : array array of vertical level depths voltri : array array with 2d volume of triangles alpha : float Euler angle alpha beta : float Euler angle beta gamma : float Euler angle gamma Returns ------- mesh : object fesom_mesh object """ def __init__(self, path, abg=[50, 15, -90], get3d=True): self.path = os.path.abspath(path) if not os.path.exists(self.path): raise IOError('The path "{}" does not exists'.format(self.path)) self.alpha = abg[0] self.beta = abg[1] self.gamma = abg[2] self.nod2dfile = os.path.join(self.path, "nod2d.out") self.elm2dfile = os.path.join(self.path, "elem2d.out") self.aux3dfile = os.path.join(self.path, "aux3d.out") self.nod3dfile = os.path.join(self.path, "nod3d.out") self.n3d = 0 self.e2d = 0 self.nlev = 0 self.zlevs = [] # self.x2= [] # self.y2= [] # self.elem= [] self.n32 = [] # self.no_cyclic_elem=[] self.topo = [] self.voltri = [] logging.info("load 2d part of the grid") start = datetime.now() self.read2d() end = datetime.now() print("Load 2d part of the grid in {} second(s)".format(end - start)) if get3d: logging.info("load 3d part of the grid") start = datetime.now() self.read3d() end = datetime.now() print("Load 3d part of the grid in {} seconds".format(end - start))
[docs] def read2d(self): """Reads only surface part of the mesh. Useful if your mesh is large and you want to visualize only surface. """ file_content = pd.read_csv( self.nod2dfile, delim_whitespace=True, skiprows=1, names=["node_number", "x", "y", "flag"], ) self.x2 = file_content.x.values self.y2 = file_content.y.values self.ind2d = file_content.flag.values self.n2d = len(self.x2) file_content = pd.read_csv( self.elm2dfile, delim_whitespace=True, skiprows=1, names=["first_elem", "second_elem", "third_elem"], ) self.elem = file_content.values - 1 self.e2d = np.shape(self.elem)[0] ########################################### # here we compute the volumes of the triangles # this should be moved into fesom generan mesh output netcdf file # r_earth = 6371000.0 rad = np.pi / 180 edx = self.x2[self.elem] edy = self.y2[self.elem] ed = np.array([edx, edy]) jacobian2D = ed[:, :, 1] - ed[:, :, 0] jacobian2D = np.array([jacobian2D, ed[:, :, 2] - ed[:, :, 0]]) for j in range(2): mind = [i for (i, val) in enumerate(jacobian2D[j, 0, :]) if val > 355] pind = [i for (i, val) in enumerate(jacobian2D[j, 0, :]) if val < -355] jacobian2D[j, 0, mind] = jacobian2D[j, 0, mind] - 360 jacobian2D[j, 0, pind] = jacobian2D[j, 0, pind] + 360 jacobian2D = jacobian2D * r_earth * rad for k in range(2): jacobian2D[k, 0, :] = jacobian2D[k, 0, :] * np.cos(edy * rad).mean(axis=1) self.voltri = abs(np.linalg.det(np.rollaxis(jacobian2D, 2))) / 2.0 # compute the 2D lump operator # cnt = np.array((0,) * self.n2d) self.lump2 = np.array((0.0,) * self.n2d) for i in range(3): for j in range(self.e2d): n = self.elem[j, i] # cnt[n]=cnt[n]+1 self.lump2[n] = self.lump2[n] + self.voltri[j] self.lump2 = self.lump2 / 3.0 self.x2, self.y2 = scalar_r2g( self.alpha, self.beta, self.gamma, self.x2, self.y2 ) d = self.x2[self.elem].max(axis=1) - self.x2[self.elem].min(axis=1) self.no_cyclic_elem = [i for (i, val) in enumerate(d) if val < 100] return self
[docs] def read3d(self): """ Reads 3d part of the mesh. """ self.n3d = int(open(self.nod3dfile).readline().rstrip()) df = pd.read_csv( self.nod3dfile, delim_whitespace=True, skiprows=1, names=["node_number", "x", "y", "z", "flag"], ) zcoord = -df.z.values self.zlevs = np.unique(zcoord) with open(self.aux3dfile) as f: self.nlev = int(next(f)) self.n32 = np.fromiter( f, dtype=np.int32, count=self.n2d * self.nlev ).reshape(self.n2d, self.nlev) self.topo = np.zeros(shape=(self.n2d)) for prof in self.n32: ind_nan = prof[prof > 0] ind_nan = ind_nan[-1] self.topo[prof[0] - 1] = zcoord[ind_nan - 1] return self
def __repr__(self): meshinfo = """ FESOM mesh: path = {} alpha, beta, gamma = {}, {}, {} number of 2d nodes = {} number of 2d elements = {} number of 3d nodes = {} """.format( self.path, str(self.alpha), str(self.beta), str(self.gamma), str(self.n2d), str(self.e2d), str(self.n3d), ) return meshinfo def __str__(self): return self.__repr__()
[docs] def ind_for_depth(depth, mesh): """ Return indexes that belong to certain depth. Parameters ---------- depth : float desired depth. Note there will be no interpolation, the model level that is closest to desired depth will be selected. mesh : object FESOM mesh object Returns ------- ind_depth : 1d array vector with the size equal to the size of the surface nodes with index values where we have data values and missing values where we don't have data values. ind_noempty : 1d array vector with indexes of the `ind_depth` that have data values. ind_empty : 1d array vector with indexes of the `ind_depth` that do not have data values. """ # Find indexes of the model depth that are closest to the required depth. dind = (abs(mesh.zlevs - depth)).argmin() # select data from the level and find indexes with values and with nans ind_depth = mesh.n32[:, dind] - 1 ind_noempty = np.where(ind_depth >= 0)[0] ind_empty = np.where(ind_depth < 0)[0] return ind_depth, ind_noempty, ind_empty