Source code for bordado._utils

# Copyright (c) 2025 The Bordado 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)
#
"""
Functions for validating and manipulation coordinate arrays.
"""

import numpy as np

from ._validation import check_adjust, check_region, check_shape


[docs] def spacing_to_size(start, stop, spacing, *, adjust="spacing"): """ Convert a spacing to the number of points between start and stop. Takes into account if the spacing or the interval needs to be adjusted in order to fit exactly. This is needed when the interval is not a multiple of the spacing. Parameters ---------- start : float The starting value of the sequence. stop : float The end value of the sequence. spacing : float The step size (interval) between points in the sequence. adjust : {'spacing', 'region'} Whether to adjust the spacing or the interval/region if required. Defaults to adjusting the spacing. Returns ------- size : int The number of points between start and stop. start : float The end of the interval, which may or may not have been adjusted. stop : float The end of the interval, which may or may not have been adjusted. Examples -------- If the spacing is a multiple of the interval, then the size is how many points fit in the interval and the start and stop values are maintained: >>> size, start, stop = spacing_to_size(0, 1, 0.5) >>> print(size, start, stop) 3 0 1 If the spacing is not a multiple, then it will be adjusted to fit the interval by default. In this case, then number of points remains the same: >>> size, start, stop = spacing_to_size(0, 1, 0.6) >>> print(size, start, stop) 3 0 1 Alternatively, we can ask it to adjust the region instead of the spacing between points: >>> size, start, stop = spacing_to_size(0, 1, 0.6, adjust="region") >>> print(f"{size} {start:.1f} {stop:.1f}") 3 -0.1 1.1 If the start and stop are the same, only a single point will be generated: >>> size, start, stop = spacing_to_size(1e-8, 1e-8, 1e-7) >>> print(f"{size} {start:.1g} {stop:.1g}") 1 1e-08 1e-08 """ check_adjust(adjust) # Add 1 to get the number of nodes, not segments size = int(round((stop - start) / spacing) + 1) # If the spacing >= 2 * (stop - start), it rounds to zero so we'd be # generating a single point, which isn't equivalent to adjusting the # spacing or the region. To get the appropriate behaviour of decreasing the # spacing until it fits the region or increasing the region until it fits # at least 1 spacing, we need to always round to at least 1 in the code # above. But this should only be done if the interval stop - start is not # close to zero. if size == 1 and not np.isclose(stop, start): # if size == 1: size += 1 if adjust == "region": # The size is the same but we adjust the interval so that the spacing # isn't altered when we do the linspace. required_length = (size - 1) * spacing given_length = stop - start pad = (required_length - given_length) / 2 stop = stop + pad start = start - pad return size, start, stop
[docs] def shape_to_spacing(region, shape, *, pixel_register=False): """ Calculate the spacing of a regular grid given a region and shape. The spacing is assumed to be constant along each direction but can vary between directions. Parameters ---------- region : tuple = (W, E, S, N, ...) The boundaries of a given region in Cartesian or geographic coordinates. Should have a lower and an upper boundary for each dimension of the coordinate system. shape : tuple = (..., size_SN, size_WE) The number of points in each direction of the given region, in reverse order. Must have one integer value per dimension of the region. The order of arguments is the opposite of the order of the region for compatibility with numpy's ``.shape`` attribute. pixel_register : bool If True, the coordinates will refer to the center of each grid pixel instead of the grid lines. In practice, this means that there will be one less element per dimension of the grid when compared to grid line registered. Default is False. Returns ------- spacing : tuple = (..., space_SN, space_WE) The grid spacing in each direction of the given region, in reverse order. Has one value per dimension of the region. The order of arguments is the opposite of the order of the region for compatibility with *shape*. Examples -------- For 2-dimensional grids, the region has 4 elements and the shape must have 2 elements: >>> spacing = shape_to_spacing((0, 10, -5, 1), (7, 11)) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}") 1.0, 1.0 The spacing doesn't have to be the same in each direction: >>> spacing = shape_to_spacing((0, 10, -5, 1), (14, 11)) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}") 0.5, 1.0 Notice that the spacing is in the same order as the shape: >>> spacing = shape_to_spacing((0, 10, -5, 1), (7, 21)) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}") 1.0, 0.5 Pixel registration is also supported: >>> spacing = shape_to_spacing( ... (-0.5, 10.5, -5.5, 1.5), (7, 11), pixel_register=True, ... ) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}") 1.0, 1.0 >>> spacing = shape_to_spacing( ... (-0.25, 10.25, -5.5, 1.5), (7, 21), pixel_register=True, ... ) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}") 1.0, 0.5 Grids don't have to be 2-dimensional: >>> spacing = shape_to_spacing((0, 10, -5, 1, 10, 14), (5, 7, 11)) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}, {spacing[2]:.1f}") 1.0, 1.0, 1.0 >>> spacing = shape_to_spacing( ... (-0.25, 10.25, -5.5, 1.5, -0.1, 1.1), (5, 7, 21), pixel_register=True, ... ) >>> print(f"{spacing[0]:.1f}, {spacing[1]:.1f}, {spacing[2]:.1f}") 0.2, 1.0, 0.5 """ check_region(region) check_shape(shape, region) spacing = [] for i, n in enumerate(reversed(shape)): n_points = n if not pixel_register: n_points -= 1 spacing.append((region[2 * i + 1] - region[2 * i]) / n_points) return tuple(reversed(spacing))
def make_non_dimensional_coordinates(values, shape, dtype): """ Make a list of arrays with the given values of non-dimensional coordinates. These are coordinates that are not part of the dimensions of a grid or spread of points. Think of them as extra coordinates. Parameters ---------- values : None or float or list or array The values of the non-dimensional coordinates. If None, an empty list will be returned. If a float, will generate one array of the desired shape with this value as the fill. If a list or array, will generate one array of the given shape per value in the list or array. shape : tuple The shape of the desired array(s). dtype : str A numpy compatible dtype used for the output array. Returns ------- coordinates : list List of arrays generated. Even if a single value is passed, the output will still be a list with a single array. Examples -------- Generating a single array: >>> make_non_dimensional_coordinates(10, (2,), dtype="int") [array([10, 10])] >>> make_non_dimensional_coordinates(10, (2, 2), dtype="int") [array([[10, 10], [10, 10]])] To generate multiple arrays: >>> make_non_dimensional_coordinates([10, 20], (2,), dtype="int") [array([10, 10]), array([20, 20])] >>> import numpy as np >>> make_non_dimensional_coordinates(np.array([10, 20]), (2,), dtype="int") [array([10, 10]), array([20, 20])] >>> make_non_dimensional_coordinates( ... np.array([[10], [20]]), (2,), dtype="int", ... ) [array([10, 10]), array([20, 20])] If None is given: >>> make_non_dimensional_coordinates(None, (2,), dtype="int") [] """ coordinates = [] if values is not None: for value in np.atleast_1d(values).ravel(): coordinates.append(np.full(shape, value, dtype=dtype)) return coordinates