Source code for parcels.grid

import functools
from ctypes import c_double
from ctypes import c_float
from ctypes import c_int
from ctypes import c_void_p
from ctypes import cast
from ctypes import POINTER
from ctypes import pointer
from ctypes import Structure
from enum import IntEnum

import numpy as np

from parcels.tools.converters import TimeConverter
from parcels.tools.loggers import logger

__all__ = ['GridCode', 'RectilinearZGrid', 'RectilinearSGrid', 'CurvilinearZGrid', 'CurvilinearSGrid', 'CGrid', 'Grid']


[docs]class GridCode(IntEnum): RectilinearZGrid = 0 RectilinearSGrid = 1 CurvilinearZGrid = 2 CurvilinearSGrid = 3
[docs]class CGrid(Structure): _fields_ = [('gtype', c_int), ('grid', c_void_p)]
[docs]class Grid(object): """Grid class that defines a (spatial and temporal) grid on which Fields are defined """ def __init__(self, lon, lat, time, time_origin, mesh): self.lon = lon self.lat = lat self.time = np.zeros(1, dtype=np.float64) if time is None else time if not self.lon.dtype == np.float32: logger.warning_once("Casting lon data to np.float32") self.lon = self.lon.astype(np.float32) if not self.lat.dtype == np.float32: logger.warning_once("Casting lat data to np.float32") self.lat = self.lat.astype(np.float32) if not self.time.dtype == np.float64: assert isinstance(self.time[0], (np.integer, np.floating, float, int)), 'Time vector must be an array of int or floats' logger.warning_once("Casting time data to np.float64") self.time = self.time.astype(np.float64) self.time_full = self.time # needed for deferred_loaded Fields self.time_origin = TimeConverter() if time_origin is None else time_origin assert isinstance(self.time_origin, TimeConverter), 'time_origin needs to be a TimeConverter object' self.mesh = mesh self.cstruct = None self.cell_edge_sizes = {} self.zonal_periodic = False self.zonal_halo = 0 self.meridional_halo = 0 self.lat_flipped = False self.defer_load = False self.lonlat_minmax = np.array([np.nanmin(lon), np.nanmax(lon), np.nanmin(lat), np.nanmax(lat)], dtype=np.float32) self.periods = 0 self.load_chunk = [] self.chunk_info = None self.chunksize = None self._add_last_periodic_data_timestep = False self.depth_field = None @staticmethod def create_grid(lon, lat, depth, time, time_origin, mesh, **kwargs): if not isinstance(lon, np.ndarray): lon = np.array(lon) if not isinstance(lat, np.ndarray): lat = np.array(lat) if not (depth is None or isinstance(depth, np.ndarray)): depth = np.array(depth) if len(lon.shape) <= 1: if depth is None or len(depth.shape) <= 1: return RectilinearZGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs) else: return RectilinearSGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs) else: if depth is None or len(depth.shape) <= 1: return CurvilinearZGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs) else: return CurvilinearSGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs) @property def ctypes_struct(self): # This is unnecessary for the moment, but it could be useful when going will fully unstructured grids self.cgrid = cast(pointer(self.child_ctypes_struct), c_void_p) cstruct = CGrid(self.gtype, self.cgrid.value) return cstruct @property def child_ctypes_struct(self): """Returns a ctypes struct object containing all relevant pointers and sizes for this grid.""" class CStructuredGrid(Structure): # z4d is only to have same cstruct as RectilinearSGrid _fields_ = [('xdim', c_int), ('ydim', c_int), ('zdim', c_int), ('tdim', c_int), ('z4d', c_int), ('mesh_spherical', c_int), ('zonal_periodic', c_int), ('chunk_info', POINTER(c_int)), ('load_chunk', POINTER(c_int)), ('tfull_min', c_double), ('tfull_max', c_double), ('periods', POINTER(c_int)), ('lonlat_minmax', POINTER(c_float)), ('lon', POINTER(c_float)), ('lat', POINTER(c_float)), ('depth', POINTER(c_float)), ('time', POINTER(c_double)) ] # Create and populate the c-struct object if not self.cstruct: # Not to point to the same grid various times if grid in various fields if not isinstance(self.periods, c_int): self.periods = c_int() self.periods.value = 0 self.cstruct = CStructuredGrid(self.xdim, self.ydim, self.zdim, self.tdim, self.z4d, self.mesh == 'spherical', self.zonal_periodic, (c_int * len(self.chunk_info))(*self.chunk_info), self.load_chunk.ctypes.data_as(POINTER(c_int)), self.time_full[0], self.time_full[-1], pointer(self.periods), self.lonlat_minmax.ctypes.data_as(POINTER(c_float)), self.lon.ctypes.data_as(POINTER(c_float)), self.lat.ctypes.data_as(POINTER(c_float)), self.depth.ctypes.data_as(POINTER(c_float)), self.time.ctypes.data_as(POINTER(c_double))) return self.cstruct def lon_grid_to_target(self): if self.lon_remapping: self.lon = self.lon_remapping.to_target(self.lon) def lon_grid_to_source(self): if self.lon_remapping: self.lon = self.lon_remapping.to_source(self.lon) def lon_particle_to_target(self, lon): if self.lon_remapping: return self.lon_remapping.particle_to_target(lon) return lon def advancetime(self, grid_new): assert isinstance(grid_new.time_origin, type(self.time_origin)), 'time_origin of new and old grids must be either both None or both a date' if self.time_origin: grid_new.time = grid_new.time + self.time_origin.reltime(grid_new.time_origin) if len(grid_new.time) != 1: raise RuntimeError('New FieldSet needs to have only one snapshot') if grid_new.time > self.time[-1]: # forward in time, so appending at end self.time = np.concatenate((self.time[1:], grid_new.time)) return 1 elif grid_new.time < self.time[0]: # backward in time, so prepending at start self.time = np.concatenate((grid_new.time, self.time[:-1])) return -1 else: raise RuntimeError("Time of field_new in Field.advancetime() overlaps with times in old Field") def check_zonal_periodic(self): if self.zonal_periodic or self.mesh == 'flat' or self.lon.size == 1: return dx = (self.lon[1:] - self.lon[:-1]) if len(self.lon.shape) == 1 else self.lon[0, 1:] - self.lon[0, :-1] dx = np.where(dx < -180, dx+360, dx) dx = np.where(dx > 180, dx-360, dx) self.zonal_periodic = sum(dx) > 359.9 def add_Sdepth_periodic_halo(self, zonal, meridional, halosize): if zonal: if len(self.depth.shape) == 3: self.depth = np.concatenate((self.depth[:, :, -halosize:], self.depth, self.depth[:, :, 0:halosize]), axis=len(self.depth.shape) - 1) assert self.depth.shape[2] == self.xdim, "Third dim must be x." else: self.depth = np.concatenate((self.depth[:, :, :, -halosize:], self.depth, self.depth[:, :, :, 0:halosize]), axis=len(self.depth.shape) - 1) assert self.depth.shape[3] == self.xdim, "Fourth dim must be x." if meridional: if len(self.depth.shape) == 3: self.depth = np.concatenate((self.depth[:, -halosize:, :], self.depth, self.depth[:, 0:halosize, :]), axis=len(self.depth.shape) - 2) assert self.depth.shape[1] == self.ydim, "Second dim must be y." else: self.depth = np.concatenate((self.depth[:, :, -halosize:, :], self.depth, self.depth[:, :, 0:halosize, :]), axis=len(self.depth.shape) - 2) assert self.depth.shape[2] == self.ydim, "Third dim must be y." def computeTimeChunk(self, f, time, signdt): nextTime_loc = np.infty if signdt >= 0 else -np.infty periods = self.periods.value if isinstance(self.periods, c_int) else self.periods prev_time_indices = self.time if self.update_status == 'not_updated': if self.ti >= 0: if time - periods*(self.time_full[-1]-self.time_full[0]) < self.time[0] or time - periods*(self.time_full[-1]-self.time_full[0]) > self.time[1]: self.ti = -1 # reset elif signdt >= 0 and (time - periods*(self.time_full[-1]-self.time_full[0]) < self.time_full[0] or time - periods*(self.time_full[-1]-self.time_full[0]) >= self.time_full[-1]): self.ti = -1 # reset elif signdt < 0 and (time - periods*(self.time_full[-1]-self.time_full[0]) <= self.time_full[0] or time - periods*(self.time_full[-1]-self.time_full[0]) > self.time_full[-1]): self.ti = -1 # reset elif signdt >= 0 and time - periods*(self.time_full[-1]-self.time_full[0]) >= self.time[1] and self.ti < len(self.time_full)-2: self.ti += 1 self.time = self.time_full[self.ti:self.ti+2] self.update_status = 'updated' elif signdt < 0 and time - periods*(self.time_full[-1]-self.time_full[0]) <= self.time[0] and self.ti > 0: self.ti -= 1 self.time = self.time_full[self.ti:self.ti+2] self.update_status = 'updated' if self.ti == -1: self.time = self.time_full self.ti, _ = f.time_index(time) periods = self.periods.value if isinstance(self.periods, c_int) else self.periods if signdt == -1 and self.ti == 0 and (time - periods*(self.time_full[-1]-self.time_full[0])) == self.time[0] and f.time_periodic: self.ti = len(self.time)-1 periods -= 1 if signdt == -1 and self.ti > 0: self.ti -= 1 if self.ti >= len(self.time_full) - 1: self.ti = len(self.time_full) - 2 self.time = self.time_full[self.ti:self.ti+2] self.tdim = 2 if prev_time_indices is None or len(prev_time_indices) != 2 or len(prev_time_indices) != len(self.time): self.update_status = 'first_updated' elif functools.reduce(lambda i, j: i and j, map(lambda m, k: m == k, self.time, prev_time_indices), True) and len(prev_time_indices) == len(self.time): self.update_status = 'not_updated' elif functools.reduce(lambda i, j: i and j, map(lambda m, k: m == k, self.time[:1], prev_time_indices[:1]), True) and len(prev_time_indices) == len(self.time): self.update_status = 'updated' else: self.update_status = 'first_updated' if signdt >= 0 and (self.ti < len(self.time_full)-2 or not f.allow_time_extrapolation): nextTime_loc = self.time[1] + periods*(self.time_full[-1]-self.time_full[0]) elif signdt < 0 and (self.ti > 0 or not f.allow_time_extrapolation): nextTime_loc = self.time[0] + periods*(self.time_full[-1]-self.time_full[0]) return nextTime_loc @property def chunk_not_loaded(self): return 0 @property def chunk_loading_requested(self): return 1 @property def chunk_loaded_touched(self): return 2 @property def chunk_deprecated(self): return 3 @property def chunk_loaded(self): return [2, 3]
class RectilinearGrid(Grid): """Rectilinear Grid Private base class for RectilinearZGrid and RectilinearSGrid """ def __init__(self, lon, lat, time, time_origin, mesh): assert(isinstance(lon, np.ndarray) and len(lon.shape) <= 1), 'lon is not a numpy vector' assert(isinstance(lat, np.ndarray) and len(lat.shape) <= 1), 'lat is not a numpy vector' assert (isinstance(time, np.ndarray) or not time), 'time is not a numpy array' if isinstance(time, np.ndarray): assert(len(time.shape) == 1), 'time is not a vector' super(RectilinearGrid, self).__init__(lon, lat, time, time_origin, mesh) self.xdim = self.lon.size self.ydim = self.lat.size self.tdim = self.time.size if self.ydim > 1 and self.lat[-1] < self.lat[0]: self.lat = np.flip(self.lat, axis=0) self.lat_flipped = True logger.warning_once("Flipping lat data from North-South to South-North") def add_periodic_halo(self, zonal, meridional, halosize=5): """Add a 'halo' to the Grid, through extending the Grid (and lon/lat) similarly to the halo created for the Fields :param zonal: Create a halo in zonal direction (boolean) :param meridional: Create a halo in meridional direction (boolean) :param halosize: size of the halo (in grid points). Default is 5 grid points """ if zonal: lonshift = (self.lon[-1] - 2 * self.lon[0] + self.lon[1]) if not np.allclose(self.lon[1]-self.lon[0], self.lon[-1]-self.lon[-2]): logger.warning_once("The zonal halo is located at the east and west of current grid, with a dx = lon[1]-lon[0] between the last nodes of the original grid and the first ones of the halo. In your grid, lon[1]-lon[0] != lon[-1]-lon[-2]. Is the halo computed as you expect?") self.lon = np.concatenate((self.lon[-halosize:] - lonshift, self.lon, self.lon[0:halosize] + lonshift)) self.xdim = self.lon.size self.zonal_periodic = True self.zonal_halo = halosize if meridional: if not np.allclose(self.lat[1]-self.lat[0], self.lat[-1]-self.lat[-2]): logger.warning_once("The meridional halo is located at the north and south of current grid, with a dy = lat[1]-lat[0] between the last nodes of the original grid and the first ones of the halo. In your grid, lat[1]-lat[0] != lat[-1]-lat[-2]. Is the halo computed as you expect?") latshift = (self.lat[-1] - 2 * self.lat[0] + self.lat[1]) self.lat = np.concatenate((self.lat[-halosize:] - latshift, self.lat, self.lat[0:halosize] + latshift)) self.ydim = self.lat.size self.meridional_halo = halosize self.lonlat_minmax = np.array([np.nanmin(self.lon), np.nanmax(self.lon), np.nanmin(self.lat), np.nanmax(self.lat)], dtype=np.float32) if isinstance(self, RectilinearSGrid): self.add_Sdepth_periodic_halo(zonal, meridional, halosize)
[docs]class RectilinearZGrid(RectilinearGrid): """Rectilinear Z Grid :param lon: Vector containing the longitude coordinates of the grid :param lat: Vector containing the latitude coordinates of the grid :param depth: Vector containing the vertical coordinates of the grid, which are z-coordinates. The depth of the different layers is thus constant. :param time: Vector containing the time coordinates of the grid :param time_origin: Time origin (TimeConverter object) of the time axis :param mesh: String indicating the type of mesh coordinates and units used during velocity interpolation: 1. spherical (default): Lat and lon in degree, with a correction for zonal velocity U near the poles. 2. flat: No conversion, lat/lon are assumed to be in m. """ def __init__(self, lon, lat, depth=None, time=None, time_origin=None, mesh='flat'): super(RectilinearZGrid, self).__init__(lon, lat, time, time_origin, mesh) if isinstance(depth, np.ndarray): assert(len(depth.shape) <= 1), 'depth is not a vector' self.gtype = GridCode.RectilinearZGrid self.depth = np.zeros(1, dtype=np.float32) if depth is None else depth self.zdim = self.depth.size self.z4d = -1 # only used in RectilinearSGrid if not self.depth.dtype == np.float32: logger.warning_once("Casting depth data to np.float32") self.depth = self.depth.astype(np.float32)
[docs]class RectilinearSGrid(RectilinearGrid): """Rectilinear S Grid. Same horizontal discretisation as a rectilinear z grid, but with s vertical coordinates :param lon: Vector containing the longitude coordinates of the grid :param lat: Vector containing the latitude coordinates of the grid :param depth: 4D (time-evolving) or 3D (time-independent) array containing the vertical coordinates of the grid, which are s-coordinates. s-coordinates can be terrain-following (sigma) or iso-density (rho) layers, or any generalised vertical discretisation. The depth of each node depends then on the horizontal position (lon, lat), the number of the layer and the time is depth is a 4D array. depth array is either a 4D array[xdim][ydim][zdim][tdim] or a 3D array[xdim][ydim[zdim]. :param time: Vector containing the time coordinates of the grid :param time_origin: Time origin (TimeConverter object) of the time axis :param mesh: String indicating the type of mesh coordinates and units used during velocity interpolation: 1. spherical (default): Lat and lon in degree, with a correction for zonal velocity U near the poles. 2. flat: No conversion, lat/lon are assumed to be in m. """ def __init__(self, lon, lat, depth, time=None, time_origin=None, mesh='flat'): super(RectilinearSGrid, self).__init__(lon, lat, time, time_origin, mesh) assert(isinstance(depth, np.ndarray) and len(depth.shape) in [3, 4]), 'depth is not a 3D or 4D numpy array' self.gtype = GridCode.RectilinearSGrid self.depth = depth self.zdim = self.depth.shape[-3] self.z4d = len(self.depth.shape) == 4 if self.z4d: # self.depth.shape[0] is 0 for S grids loaded from netcdf file assert self.tdim == self.depth.shape[0] or self.depth.shape[0] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' assert self.xdim == self.depth.shape[-1] or self.depth.shape[-1] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' assert self.ydim == self.depth.shape[-2] or self.depth.shape[-2] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' else: assert self.xdim == self.depth.shape[-1], 'depth dimension has the wrong format. It should be [zdim, ydim, xdim]' assert self.ydim == self.depth.shape[-2], 'depth dimension has the wrong format. It should be [zdim, ydim, xdim]' if not self.depth.dtype == np.float32: logger.warning_once("Casting depth data to np.float32") self.depth = self.depth.astype(np.float32) if self.lat_flipped: self.depth = np.flip(self.depth, axis=-2)
class CurvilinearGrid(Grid): def __init__(self, lon, lat, time=None, time_origin=None, mesh='flat'): assert(isinstance(lon, np.ndarray) and len(lon.squeeze().shape) == 2), 'lon is not a 2D numpy array' assert(isinstance(lat, np.ndarray) and len(lat.squeeze().shape) == 2), 'lat is not a 2D numpy array' assert (isinstance(time, np.ndarray) or not time), 'time is not a numpy array' if isinstance(time, np.ndarray): assert(len(time.shape) == 1), 'time is not a vector' lon = lon.squeeze() lat = lat.squeeze() super(CurvilinearGrid, self).__init__(lon, lat, time, time_origin, mesh) self.xdim = self.lon.shape[1] self.ydim = self.lon.shape[0] self.tdim = self.time.size def add_periodic_halo(self, zonal, meridional, halosize=5): """Add a 'halo' to the Grid, through extending the Grid (and lon/lat) similarly to the halo created for the Fields :param zonal: Create a halo in zonal direction (boolean) :param meridional: Create a halo in meridional direction (boolean) :param halosize: size of the halo (in grid points). Default is 5 grid points """ if zonal: lonshift = self.lon[:, -1] - 2 * self.lon[:, 0] + self.lon[:, 1] if not np.allclose(self.lon[:, 1]-self.lon[:, 0], self.lon[:, -1]-self.lon[:, -2]): logger.warning_once("The zonal halo is located at the east and west of current grid, with a dx = lon[:,1]-lon[:,0] between the last nodes of the original grid and the first ones of the halo. In your grid, lon[:,1]-lon[:,0] != lon[:,-1]-lon[:,-2]. Is the halo computed as you expect?") self.lon = np.concatenate((self.lon[:, -halosize:] - lonshift[:, np.newaxis], self.lon, self.lon[:, 0:halosize] + lonshift[:, np.newaxis]), axis=len(self.lon.shape)-1) self.lat = np.concatenate((self.lat[:, -halosize:], self.lat, self.lat[:, 0:halosize]), axis=len(self.lat.shape)-1) self.xdim = self.lon.shape[1] self.ydim = self.lat.shape[0] self.zonal_periodic = True self.zonal_halo = halosize if meridional: if not np.allclose(self.lat[1, :]-self.lat[0, :], self.lat[-1, :]-self.lat[-2, :]): logger.warning_once("The meridional halo is located at the north and south of current grid, with a dy = lat[1,:]-lat[0,:] between the last nodes of the original grid and the first ones of the halo. In your grid, lat[1,:]-lat[0,:] != lat[-1,:]-lat[-2,:]. Is the halo computed as you expect?") latshift = self.lat[-1, :] - 2 * self.lat[0, :] + self.lat[1, :] self.lat = np.concatenate((self.lat[-halosize:, :] - latshift[np.newaxis, :], self.lat, self.lat[0:halosize, :] + latshift[np.newaxis, :]), axis=len(self.lat.shape)-2) self.lon = np.concatenate((self.lon[-halosize:, :], self.lon, self.lon[0:halosize, :]), axis=len(self.lon.shape)-2) self.xdim = self.lon.shape[1] self.ydim = self.lat.shape[0] self.meridional_halo = halosize if isinstance(self, CurvilinearSGrid): self.add_Sdepth_periodic_halo(zonal, meridional, halosize)
[docs]class CurvilinearZGrid(CurvilinearGrid): """Curvilinear Z Grid. :param lon: 2D array containing the longitude coordinates of the grid :param lat: 2D array containing the latitude coordinates of the grid :param depth: Vector containing the vertical coordinates of the grid, which are z-coordinates. The depth of the different layers is thus constant. :param time: Vector containing the time coordinates of the grid :param time_origin: Time origin (TimeConverter object) of the time axis :param mesh: String indicating the type of mesh coordinates and units used during velocity interpolation: 1. spherical (default): Lat and lon in degree, with a correction for zonal velocity U near the poles. 2. flat: No conversion, lat/lon are assumed to be in m. """ def __init__(self, lon, lat, depth=None, time=None, time_origin=None, mesh='flat'): super(CurvilinearZGrid, self).__init__(lon, lat, time, time_origin, mesh) if isinstance(depth, np.ndarray): assert(len(depth.shape) == 1), 'depth is not a vector' self.gtype = GridCode.CurvilinearZGrid self.depth = np.zeros(1, dtype=np.float32) if depth is None else depth self.zdim = self.depth.size self.z4d = -1 # only for SGrid if not self.depth.dtype == np.float32: logger.warning_once("Casting depth data to np.float32") self.depth = self.depth.astype(np.float32)
[docs]class CurvilinearSGrid(CurvilinearGrid): """Curvilinear S Grid. :param lon: 2D array containing the longitude coordinates of the grid :param lat: 2D array containing the latitude coordinates of the grid :param depth: 4D (time-evolving) or 3D (time-independent) array containing the vertical coordinates of the grid, which are s-coordinates. s-coordinates can be terrain-following (sigma) or iso-density (rho) layers, or any generalised vertical discretisation. The depth of each node depends then on the horizontal position (lon, lat), the number of the layer and the time is depth is a 4D array. depth array is either a 4D array[xdim][ydim][zdim][tdim] or a 3D array[xdim][ydim[zdim]. :param time: Vector containing the time coordinates of the grid :param time_origin: Time origin (TimeConverter object) of the time axis :param mesh: String indicating the type of mesh coordinates and units used during velocity interpolation: 1. spherical (default): Lat and lon in degree, with a correction for zonal velocity U near the poles. 2. flat: No conversion, lat/lon are assumed to be in m. """ def __init__(self, lon, lat, depth, time=None, time_origin=None, mesh='flat'): super(CurvilinearSGrid, self).__init__(lon, lat, time, time_origin, mesh) assert(isinstance(depth, np.ndarray) and len(depth.shape) in [3, 4]), 'depth is not a 4D numpy array' self.gtype = GridCode.CurvilinearSGrid self.depth = depth self.zdim = self.depth.shape[-3] self.z4d = len(self.depth.shape) == 4 if self.z4d: # self.depth.shape[0] is 0 for S grids loaded from netcdf file assert self.tdim == self.depth.shape[0] or self.depth.shape[0] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' assert self.xdim == self.depth.shape[-1] or self.depth.shape[-1] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' assert self.ydim == self.depth.shape[-2] or self.depth.shape[-2] == 0, 'depth dimension has the wrong format. It should be [tdim, zdim, ydim, xdim]' else: assert self.xdim == self.depth.shape[-1], 'depth dimension has the wrong format. It should be [zdim, ydim, xdim]' assert self.ydim == self.depth.shape[-2], 'depth dimension has the wrong format. It should be [zdim, ydim, xdim]' if not self.depth.dtype == np.float32: logger.warning_once("Casting depth data to np.float32") self.depth = self.depth.astype(np.float32)