Source code for harmonica._forward.tesseroid_layer

# Copyright (c) 2018 The Harmonica Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
#
# This code is part of the Fatiando a Terra project (https://www.fatiando.org)
#
"""
Define a layer of tesseroids
"""
import warnings

import numpy as np
import verde as vd
import xarray as xr

from .tesseroid import tesseroid_gravity


[docs]def tesseroid_layer(coordinates, surface, reference, properties=None): """ Create a layer of tesseroids of equal size Parameters ---------- coordinates : tuple List containing the coordinates of the centers of the tesseroids in spherical coordinates in the following order ``longitude`` and ``latitude``. surface : 2d-array Array used to create the uppermost boundary of the tesserois layer. All radii should be in meters. On every point where ``surface`` is below ``reference``, the ``surface`` value will be used to set the ``bottom`` boundary of that tesseroid, while the ``reference`` value will be used to set the ``top`` boundary of the tesseroid. reference : 2d-array or float Reference surface used to create the lowermost boundary of the tesseroids layer. It can be either a plane or an irregular surface passed as 2d array. Radii must be in meters. properties : dict or None Dictionary containing the physical properties of the tesseroids. The keys must be strings that will be used to name the corresponding ``data_var`` inside the :class:`xarray.Dataset`, while the values must be 2d-arrays. All physical properties must be passed in SI units. If None, no ``data_var`` will be added to the :class:`xarray.Dataset`. Default is None. Returns ------- dataset : :class:`xarray.Dataset` Dataset containing the coordinates of the center of each tesseroid, the height of its top and bottom boundaries ans its corresponding physical properties. See also -------- harmonica.DatasetAccessorsTesseroidLayer """ dims = ("latitude", "longitude") # Initialize data and data_names as None data, data_names = None, None # If properties were passed, then replace data_names and data for its keys # and values, respectively if properties: data_names = tuple(p for p in properties.keys()) data = tuple(np.asarray(p) for p in properties.values()) # Create xr.Dataset for tesseroids tesseroids = vd.make_xarray_grid( coordinates, data=data, data_names=data_names, dims=dims ) _check_regular_grid(tesseroids.longitude.values, tesseroids.latitude.values) # Check if tesseroid boundaries are overlapped _check_overlap(tesseroids.longitude.values) # Append some attributes to the xr.Dataset attrs = { "longitude_units": "degrees", "latitude_units": "degrees", "radius_units": "meters", "properties_units": "SI", } tesseroids.attrs = attrs # Create the top and bottom coordinates of the prisms tesseroids.tesseroid_layer.update_top_bottom(surface, reference) return tesseroids
def _check_regular_grid(longitude, latitude): """ Check if the longitude and latitude coordinates define a regular grid .. note: This function should live inside Verde in the future """ if not np.allclose(longitude[1] - longitude[0], longitude[1:] - longitude[:-1]): raise ValueError("Passed longitude coordinates are note evenly spaced.") if not np.allclose(latitude[1] - latitude[0], latitude[1:] - latitude[:-1]): raise ValueError("Passed latitude coordinates are note evenly spaced.") def _check_overlap(longitude): """ Check if the prisms boundaries are overlapped """ spacing = longitude[1] - longitude[0] if longitude.max() - longitude.min() >= 360 - spacing: raise ValueError( "Found invalid longitude coordinates that would create overlapping tesseroids around the globe." )
[docs]@xr.register_dataset_accessor("tesseroid_layer") class DatasetAccessorTesseroidLayer: """ Define dataset accessor for layer of tesseroids .. warning:: This class in not intended to be initialized. Use the `tesseroid_layer` accessor for accessing the methods and attributes of this class. See also -------- harmonica.tesseroid_layer """ def __init__(self, xarray_obj): self._obj = xarray_obj @property def dims(self): """ Return the dims tuple of the prism layer The tuple follows the xarray order: ``"latitude"``, ``"longitude"``. """ return ("latitude", "longitude") @property def spacing(self): """ Spacing between center of tesseroids Returns ------- s_latitude : float Spacing between center of the tesseroids on the latitude direction. s_longitude : float Spacing between center of the tesseroids on the longitude direction. """ latitude, longitude = self._obj.latitude.values, self._obj.longitude.values _check_regular_grid(longitude, latitude) s_latitude, s_longitude = latitude[1] - latitude[0], longitude[1] - longitude[0] return s_latitude, s_longitude @property def size(self): """ Return the total number of tesseroids on the layer Returns ------- size : int Total number of tesseroids in the layer. """ return self._obj.latitude.size * self._obj.longitude.size @property def shape(self): """ Return the number of tesseroids on each directions Returns ------- n_latitude : int Number of tesseroids on the latitude direction. n_longitude : int Number of tesserods on the longitude direction. """ return (self._obj.latitude.size, self._obj.longitude.size) @property def boundaries(self): """ Boundaries of the layer Returns ------- boundaries : tuple Boundaries of the layer of tesseroids in the following order: ``longitude_w``, ``longitude_e``, ``latitude_s``, ``latitude_n`` """ s_latitude, s_longitude = self.spacing longitude_w = self._obj.longitude.values.min() - s_longitude / 2 longitude_e = self._obj.longitude.values.max() + s_longitude / 2 latitude_s = self._obj.latitude.values.min() - s_latitude / 2 latitude_n = self._obj.latitude.values.max() + s_latitude / 2 return longitude_w, longitude_e, latitude_s, latitude_n
[docs] def update_top_bottom(self, surface, reference): """ Update top and bottom boundaries of the layer Change the values of the ``top`` and ``bottom`` coordinates based on the passed ``surface`` and ``reference``. The ``top`` and ``bottom`` boundaries of every tesseroid will be equal to the corresponding ``surface`` and ``reference`` values, respectively, if ``surface`` is above the ``reference`` on that point. Otherwise the ``top`` and ``bottom`` boundaries of the tesseroid will be equal to its corresponding ``reference`` and ``surface``, respectively. Parameters ---------- surface : 2d-array Array used to create the uppermost boundary of the tesseroid layer. All heights should be in meters. On every point where ``surface`` is below ``reference``, the ``surface`` value will be used to set the ``bottom`` boundary of that tesseroid, while the ``reference`` value will be used to set the ``top`` boundary of the tesseroid. reference : 2d-array or float Reference surface used to create the lowermost boundary of the tesseroid layer. It can be either a plane or an irregular surface passed as 2d array. Height(s) must be in meters. """ surface, reference = np.asarray(surface), np.asarray(reference) if surface.shape != self.shape: raise ValueError( f"Invalid surface array with shape '{surface.shape}'. " + "Its shape should be compatible with the coordinates " + "of the layer of tesseroids." ) if reference.ndim != 0: if reference.shape != self.shape: raise ValueError( f"Invalid reference array with shape '{reference.shape}'. " + "Its shape should be compatible with the coordinates " + "of the layer of tesseroids." ) else: reference = reference * np.ones(self.shape) top = surface.copy() bottom = reference.copy() reverse = surface < reference top[reverse] = reference[reverse] bottom[reverse] = surface[reverse] self._obj.coords["top"] = (self.dims, top) self._obj.coords["bottom"] = (self.dims, bottom)
[docs] def gravity(self, coordinates, field, density_name="density", **kwargs): """ Computes the gravity generated by the layer of tesseroids Parameters ---------- coordinates : list or 1d-array List of array containing ``latitude``, ``longitude`` and ``upward`` of the computation points defined on a spherical coordinates system. ``upward`` coordinate should be in meters. field : str Gravitational field that wants to be computed. The variable fields are: - Gravitational potential: ``potential`` - Downward acceleration: ``g_z`` density_name : str (optional) Name of the property layer (or ``data_var`` of the :class:`xarray.Dataset`) that will be used for the density of each tesseroid in the layer. Default to ``"density"`` Returns ------- result : array Gravitational field generated by the tesseroid on the computation point in mGal See also -------- harmonica.tesseroid_gravity """ # Get boundaries and density of the tesseroids boundaries = self._to_tesseroids() density = self._obj[density_name].values # Get the mask for selecting only the tesseroid whose top boundary, # bottom boundary and density have no nans mask = self._get_nonans_mask(property_name=density_name) # Select only the boundaries and density elements for masked tesseroid boundaries = boundaries[mask.ravel()] density = density[mask] # Return gravity field of tesserids return tesseroid_gravity( coordinates, tesseroids=boundaries, density=density, field=field, **kwargs, )
def _get_nonans_mask(self, property_name=None): """ Build a mask for tesseroid with no nans on top, bottom or a property Parameters ---------- mask : 2d-array Array of bools that can be used as a mask for selecting tesseroids with no nans on top boundaries, bottom boundaries ans the passed property. """ # Mask the tesseroid that contains no nans on top and bottom boundaries mask = np.logical_and( np.logical_not(np.isnan(self._obj.top.values)), np.logical_not(np.isnan(self._obj.bottom.values)), ) # Mask the tesseroids that contains nans on the selected property if property_name is not None: mask_property = np.logical_not(np.isnan(self._obj[property_name].values)) # Warn if a nan is found within the masked property if not mask_property[mask].all(): warnings.warn( 'Found missing values in "{}" property '.format(property_name) + "of the tesseroid layer. " + "The tesseroids with nan as " + '"{}" will be ignored.'.format(property_name) ) mask = np.logical_and(mask, mask_property) return mask def _to_tesseroids(self): """ Return the boundaries of each tesseroid of the layer Returns ------- tesseroids : 2d-array Array containing the boundaries of each tesseroid of the layer. Each row contains the boundaries of each tesseroid in the following order: ``longitude_w``, ``longitude_e``, ``latitude_s``, ``latitude_n``, ``bottom``, ``top``. """ longitude, latitude = np.meshgrid( self._obj.longitude.values, self._obj.latitude.values ) ( longitude_w, longitude_e, latitude_s, latitude_n, ) = self._get_tesseroid_horizontal_boundaries( longitude.ravel(), latitude.ravel() ) bottom = self._obj.bottom.values.ravel() top = self._obj.top.values.ravel() tesseroids = np.vstack( (longitude_w, longitude_e, latitude_s, latitude_n, bottom, top) ).T return tesseroids def _get_tesseroid_horizontal_boundaries(self, longitude, latitude): """ Compute the horizontal boundaries of the tesseroid Parameters ---------- latitude: float or array Longitude coordinates of the center of the tesseroid longitude : float or array Longitude coordinates of the center of the tesseroid """ spacing = self.spacing longitude_w = longitude - spacing[1] / 2 longitude_e = longitude + spacing[1] / 2 latitude_s = latitude - spacing[0] / 2 latitude_n = latitude + spacing[0] / 2 return longitude_w, longitude_e, latitude_s, latitude_n
[docs] def get_tesseroid(self, indices): """ Return the boundaries of the chosen tesseroid Parameters ---------- indices : tuple Indices of the desired tesseroid of the layer in the following order: ``(index_northing, index_easting)``. Returns ------- tesseroid : tuple Boundaries of the prisms in the following order: ``longitude_w``, ``longitude_e``, ``latitude_s``, ``latitude_n``, ``bottom``, ``top``. """ # Get the center of the tesseroid center_longitude = self._obj.longitude.values[indices[1]] center_latitude = self._obj.latitude.values[indices[0]] # Calculate the boundaries of the tesseroid # ( # longitude_w, # Longitude_e, # latitude_s, # latitude_n, boundaries = self._get_tesseroid_horizontal_boundaries( center_longitude, center_latitude ) bottom = self._obj.bottom.values[indices] top = self._obj.top.values[indices] # return longitude_w, longitude_e, latitude_s, latitude_n, bottom, top return boundaries[0], boundaries[1], boundaries[2], boundaries[3], bottom, top