Source code for pyfar.classes.coordinates

"""
The following documents the pyfar coordinates class and functions for
coordinate conversion. More background information is given in
:py:mod:`coordinates concepts <pyfar._concepts.coordinates>`.
Available sampling schemes are listed at
:py:mod:`spharpy.samplings <spharpy.samplings>`.
"""

import numpy as np
from scipy.spatial import cKDTree
from scipy.spatial.transform import Rotation as sp_rot
import re
from copy import deepcopy
import warnings
from pyfar.classes.warnings import PyfarDeprecationWarning

import pyfar as pf


[docs] class Coordinates(): """ This function will be changed in pyfar 0.8.0 and will just be able to get cartesian coordinates. If you want to initialize in an other domain use :py:func:`from_spherical_colatitude`, :py:func:`from_spherical_elevation`, :py:func:`from_spherical_front`, :py:func:`from_spherical_side`, or :py:func:`from_cylindrical` instead. For conversions from or into degree use :py:func:`deg2rad` and :py:func:`rad2deg`. Create :py:func:`Coordinates` object with or without coordinate points. The points that enter the Coordinates object are defined by the `domain`, `convention`, and `unit` as illustrated in the :py:mod:`coordinates concepts <pyfar._concepts.coordinates>`: +--------------------+----------+------------+----------+----------+ | domain, convention | points_1 | points_2 | points_3 | unit | +====================+==========+============+==========+==========+ | cart, right | x | y | z | met | +--------------------+----------+------------+----------+----------+ | sph, top_colat | azimuth | colatitude | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, top_elev | azimuth | elevation | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, side | lateral | polar | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, front | phi | theta | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | cyl, top | azimuth | z | radius_z | rad, deg | +--------------------+----------+------------+----------+----------+ Parameters ---------- points_1 : array like, number Points for the first coordinate. ``'points_1'``, ``'points_2'``, and ``'points_3'`` will be renamed to ``'x'``, ``'y'`` and ``'z'`` in pyfar 0.8.0. points_2 : array like, number Points for the second coordinate. ``'points_1'``, ``'points_2'``, and ``'points_3'`` will be renamed to ``'x'``, ``'y'`` and ``'z'`` in pyfar 0.8.0. points_3 : array like, number Points for the third coordinate. ``'points_1'``, ``'points_2'``, and ``'points_3'`` will be renamed to ``'x'``, ``'y'`` and ``'z'`` in pyfar 0.8.0. domain : string ``'domain'``, ``'unit'`` and ``'convention'`` initialization parameters will be deprecated in pyfar 0.8.0 in favor of ``from_*``. Different units are no longer supported. The unit is meter for distances and radians for angles. domain of the coordinate system ``'cart'`` Cartesian ``'sph'`` Spherical ``'cyl'`` Cylindrical The default is ``'cart'``. convention: string ``'domain'``, ``'unit'`` and ``'convention'`` initialization parameters will be deprecated in pyfar 0.8.0 in favor of ``from_*``. Different units are no longer supported. Default angle unit is radiant. Coordinate convention (see above) The default is ``'right'`` if domain is ``'cart'``, ``'top_colat'`` if domain is ``'sph'``, and ``'top'`` if domain is ``'cyl'``. unit: string ``'domain'``, ``'unit'`` and ``'convention'`` initialization parameters will be deprecated in pyfar 0.8.0 in favor of ``from_*``. Different units are no longer supported. Default angle unit is radiant. The ``'deg'`` parameter will be deprecated in pyfar 0.8.0 in favor of the :py:func:`deg2rad` and :py:func:`rad2deg`. Unit of the coordinate system. By default the first available unit is used, which is meters (``'met'``) for ``domain = 'cart'`` and radians (``'rad'``) in all other cases (See above). weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. sh_order : int, optional This property will be deprecated in pyfar 0.8.0 in favor of :py:class:`spharpy.samplings.SamplingSphere` Maximum spherical harmonic order of the sampling grid. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. """ _x: np.array = np.empty _y: np.array = np.empty _z: np.array = np.empty _weights: np.array = None _sh_order: int = None _comment: str = None _system: dict = None def __init__( self, points_1: np.array = np.asarray([]), points_2: np.array = np.asarray([]), points_3: np.array = np.asarray([]), domain: str = 'cart', convention: str = None, unit: str = None, weights: np.array = None, sh_order=None, comment: str = "") -> None: # init empty object super(Coordinates, self).__init__() # test Deprecation warning if domain != 'cart' or convention is not None or unit is not None: warnings.warn(( "This function will be changed in pyfar 0.8.0 to " "init(x, y, z)."), PyfarDeprecationWarning) # set the coordinate system system = self._make_system(domain, convention, unit) self._system = system # set coordinates according to system if domain == 'cart': self._set_points(points_1, points_2, points_3) elif domain == 'sph': self._set_sph( points_1, points_2, points_3, system['convention'], system['unit']) elif domain == 'cyl': self._set_cyl( points_1, points_2, points_3, system['convention'], system['unit']) else: raise ValueError( f"Domain for {domain} is not implemented.") # save meta data self._set_weights(weights) self._sh_order = sh_order self._comment = comment if sh_order is not None: warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of spharpy.samplings.SamplingSphere."), PyfarDeprecationWarning)
[docs] @classmethod def from_cartesian( cls, x, y, z, weights: np.array = None, comment: str = ""): r""" Create a Coordinates class object from a set of points in the right-handed cartesian coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- x : ndarray, number X coordinate of a right handed Cartesian coordinate system in meters (-\infty < x < \infty). y : ndarray, number Y coordinate of a right handed Cartesian coordinate system in meters (-\infty < y < \infty). z : ndarray, number Z coordinate of a right handed Cartesian coordinate system in meters (-\infty < z < \infty). weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_cartesian(0, 0, 1) Or the using init >>> import pyfar as pf >>> coordinates = pf.Coordinates(0, 0, 1) """ return cls(x, y, z, weights=weights, comment=comment)
[docs] @classmethod def from_spherical_elevation( cls, azimuth, elevation, radius, weights: np.array = None, comment: str = ""): """Create a Coordinates class object from a set of points in the spherical coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- azimuth : ndarray, double Angle in radiant of rotation from the x-y-plane facing towards positive x direction. Used for spherical and cylindrical coordinate systems. elevation : ndarray, double Angle in radiant with respect to horizontal plane (x-z-plane). Used for spherical coordinate systems. radius : ndarray, double Distance to origin for each point. Used for spherical coordinate systems. weights: array like, float, None, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_spherical_elevation(0, 0, 1) """ x, y, z = sph2cart(azimuth, np.pi / 2 - elevation, radius) return cls(x, y, z, weights=weights, comment=comment)
[docs] @classmethod def from_spherical_colatitude( cls, azimuth, colatitude, radius, weights: np.array = None, comment: str = ""): """Create a Coordinates class object from a set of points in the spherical coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- azimuth : ndarray, double Angle in radiant of rotation from the x-y-plane facing towards positive x direction. Used for spherical and cylindrical coordinate systems. colatitude : ndarray, double Angle in radiant with respect to polar axis (z-axis). Used for spherical coordinate systems. radius : ndarray, double Distance to origin for each point. Used for spherical coordinate systems. weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_spherical_colatitude(0, 0, 1) """ x, y, z = sph2cart(azimuth, colatitude, radius) return cls(x, y, z, weights=weights, comment=comment)
[docs] @classmethod def from_spherical_side( cls, lateral, polar, radius, weights: np.array = None, comment: str = ""): """Create a Coordinates class object from a set of points in the spherical coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- lateral : ndarray, double Angle in radiant with respect to horizontal plane (x-y-plane). Used for spherical coordinate systems. polar : ndarray, double Angle in radiant of rotation from the x-z-plane facing towards positive x direction. Used for spherical coordinate systems. radius : ndarray, double Distance to origin for each point. Used for spherical coordinate systems. weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_spherical_side(0, 0, 1) """ x, z, y = sph2cart(polar, np.pi / 2 - lateral, radius) return cls(x, y, z, weights=weights, comment=comment)
[docs] @classmethod def from_spherical_front( cls, frontal, upper, radius, weights: np.array = None, comment: str = ""): """Create a Coordinates class object from a set of points in the spherical coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- frontal : ndarray, double Angle in radiant of rotation from the y-z-plane facing towards positive y direction. Used for spherical coordinate systems. upper : ndarray, double Angle in radiant with respect to polar axis (x-axis). Used for spherical coordinate systems. radius : ndarray, double Distance to origin for each point. Used for spherical coordinate systems. weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_spherical_front(0, 0, 1) """ y, z, x = sph2cart(frontal, upper, radius) return cls(x, y, z, weights=weights, comment=comment)
[docs] @classmethod def from_cylindrical( cls, azimuth, z, rho, weights: np.array = None, comment: str = ""): """Create a Coordinates class object from a set of points in the cylindrical coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information. Parameters ---------- azimuth : ndarray, double Angle in radiant of rotation from the x-y-plane facing towards positive x direction. Used for spherical and cylindrical coordinate systems. z : ndarray, double The z coordinate rho : ndarray, double Distance to origin for each point in the x-y-plane. Used for cylindrical coordinate systems. weights: array like, number, optional Weighting factors for coordinate points. The `shape` of the array must match the `shape` of the individual coordinate arrays. The default is ``None``. comment : str, optional Comment about the stored coordinate points. The default is ``""``, which initializes an empty string. Examples -------- Create a coordinates object >>> import pyfar as pf >>> coordinates = pf.Coordinates.from_cylindrical(0, 0, 1) """ x, y, z = cyl2cart(azimuth, z, rho) return cls(x, y, z, weights=weights, comment=comment)
[docs] def set_cart(self, x, y, z, convention='right', unit='met'): """ This function will be deprecated in pyfar 0.8.0 in favor of :py:func:`cartesian`, :py:func:`x`, :py:func:`y` or :py:func:`z`. Enter coordinate points in cartesian coordinate systems. The points that enter the Coordinates object are defined by the `domain`, `convention`, and `unit` +--------------------+----------+------------+----------+----------+ | domain, convention | points_1 | points_2 | points_3 | unit | +====================+==========+============+==========+==========+ | cart, right | x | y | z | met | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- x, y, z: array like, float Points for the first, second, and third coordinate convention : string, optional Convention in which the coordinate points are stored. The default is ``'right'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'met'`` for meters. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of .cart, .x, .y or .z."), PyfarDeprecationWarning) # set the coordinate system self._system = self._make_system('cart', convention, unit) # save coordinates to self self._set_cart(x, y, z)
def _set_cart(self, x, y, z, convention='right', unit='met'): if convention != 'right': # Can not be tested. Will only be raised if a coordinate system # is not fully implemented. raise ValueError( (f"Conversion for {convention} is not implemented.")) # make array x = np.atleast_1d(np.asarray(x, dtype=np.float64)) y = np.atleast_1d(np.asarray(y, dtype=np.float64)) z = np.atleast_1d(np.asarray(z, dtype=np.float64)) # squeeze if len(x.shape) == 2 and (x.shape[0] == 1 or x.shape[1] == 1): x = x.flatten() if len(y.shape) == 2 and (y.shape[0] == 1 or y.shape[1] == 1): y = y.flatten() if len(z.shape) == 2 and (z.shape[0] == 1 or z.shape[1] == 1): z = z.flatten() # save coordinates to self self._set_points(x, y, z)
[docs] def get_cart(self, convention='right', unit='met', convert=False): """ This function will be deprecated in pyfar 0.8.0 in favor of :py:func:`cartesian` Get coordinate points in cartesian coordinate systems. The points that are returned are defined by the `domain`, `convention`, and `unit`: +--------------------+----------+------------+----------+----------+ | domain, convention | p[...,1] | p[...,1] | p[...,1] | units | +====================+==========+============+==========+==========+ | cart, right | x | y | z | met | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- convention : string, optional Convention in which the coordinate points are stored. The default is ``'right'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'met'``. convert : boolean, optional If True, the internal representation of the samplings points will be converted to the queried coordinate system. The default is ``False``, i.e., the internal presentation remains as it is. Returns ------- points : numpy array Coordinate points. ``points[...,0]`` holds the points for the first coordinate, ``points[...,1]`` the points for the second, and ``points[...,2]`` the points for the third coordinate. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of .cartesian"), PyfarDeprecationWarning) self._system = self._make_system('cart', convention, unit) return self.cartesian
[docs] def set_sph( self, angles_1, angles_2, radius, convention='top_colat', unit='rad'): """ This function will be deprecated in pyfar 0.8.0 in favor of the ``spherical_*`` properties. For conversions from or into degree use :py:func:`deg2rad` and :py:func:`rad2deg`. Enter coordinate points in spherical coordinate systems. The points that enter the Coordinates object are defined by the `domain`, `convention`, and `unit` +--------------------+----------+------------+----------+----------+ | domain, convention | points_1 | points_2 | points_3 | unit | +====================+==========+============+==========+==========+ | sph, top_colat | azimuth | colatitude | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, top_elev | azimuth | elevation | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, side | lateral | polar | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, front | phi | theta | radius | rad, deg | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- points_i: array like, number Points for the first, second, and third coordinate convention : string, optional Convention in which the coordinate points are stored. The default is ``'top_colat'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'rad'``. The ``'deg'`` parameter will be deprecated in pyfar 0.8.0 in favor of the :py:func:`deg2rad` and :py:func:`rad2deg`. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of the spherical_... properties"), PyfarDeprecationWarning) # make array angles_1 = np.atleast_1d(np.asarray(angles_1, dtype=np.float64)) angles_2 = np.atleast_1d(np.asarray(angles_2, dtype=np.float64)) radius = np.atleast_1d(np.asarray(radius, dtype=np.float64)) self._set_sph(angles_1, angles_2, radius, convention, unit)
def _set_sph( self, angles_1, angles_2, radius, convention='top_colat', unit='rad'): # Convert to array angles_1 = np.asarray(angles_1) angles_2 = np.asarray(angles_2) radius = np.asarray(radius) # squeeze if len(angles_1.shape) == 2 and \ (angles_1.shape[0] == 1 or angles_1.shape[1] == 1): angles_1 = angles_1.flatten() if len(angles_2.shape) == 2 and \ (angles_2.shape[0] == 1 or angles_2.shape[1] == 1): angles_2 = angles_2.flatten() if len(radius.shape) == 2 and \ (radius.shape[0] == 1 or radius.shape[1] == 1): radius = radius.flatten() # convert to radians if unit == 'deg': warnings.warn(( "'deg' parameter will be deprecated in pyfar 0.8.0 in favor " "of the pyfar.deg2rad and pyfar.rad2deg"), PyfarDeprecationWarning) angles_1 = angles_1 / 180 * np.pi angles_2 = angles_2 / 180 * np.pi # convert to cartesian ... # ... from spherical coordinate systems if convention == 'top_colat': x, y, z = sph2cart(angles_1, angles_2, radius) elif convention == 'top_elev': x, y, z = sph2cart(angles_1, np.pi / 2 - angles_2, radius) elif convention == 'side': x, z, y = sph2cart(angles_2, np.pi / 2 - angles_1, radius) elif convention == 'front': y, z, x = sph2cart(angles_1, angles_2, radius) else: # Can not be tested. Will only be raised if a coordinate system # is not fully implemented. raise ValueError( (f"Conversion for {convention} is not implemented.")) # set the coordinate system self._system = self._make_system('sph', convention, unit) # save coordinates to self self._set_points(x, y, z)
[docs] def get_sph(self, convention='top_colat', unit='rad', convert=False): """ This function will be deprecated in pyfar 0.8.0 in favor of the `spherical_...` properties. For conversions from or into degree use :py:func:`deg2rad` and :py:func:`rad2deg`. Get coordinate points in spherical coordinate systems. The points that are returned are defined by the `domain`, `convention`, and `unit`: +--------------------+----------+------------+----------+----------+ | domain, convention | p[...,1] | p[...,1] | p[...,1] | units | +====================+==========+============+==========+==========+ | sph, top_colat | azimuth | colatitude | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, top_elev | azimuth | elevation | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, side | lateral | polar | radius | rad, deg | +--------------------+----------+------------+----------+----------+ | sph, front | phi | theta | radius | rad, deg | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- convention : string, optional Convention in which the coordinate points are stored. The default is ``'top_colat'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'rad'``. The ``'deg'`` parameter will be deprecated in pyfar 0.8.0 in favor of the :py:func:`deg2rad` and :py:func:`rad2deg`. convert : boolean, optional If True, the internal representation of the samplings points will be converted to the queried coordinate system. The default is ``False``, i.e., the internal presentation remains as it is. Returns ------- points : numpy array Coordinate points. ``points[...,0]`` holds the points for the first coordinate, ``points[...,1]`` the points for the second, and ``points[...,2]`` the points for the third coordinate. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of the `spherical_*` properties."), PyfarDeprecationWarning) if convention == 'top_colat': points = self.spherical_colatitude self._system = self._make_system('sph', 'top_colat', 'rad') elif convention == 'top_elev': points = self.spherical_elevation self._system = self._make_system('sph', 'top_elev', 'rad') elif convention == 'front': points = self.spherical_front self._system = self._make_system('sph', 'front', 'rad') elif convention == 'side': points = self.spherical_side self._system = self._make_system('sph', 'side', 'rad') else: raise ValueError( f"Conversion for {convention} is not implemented.") conversion_factor = 1 if unit == 'rad' else 180 / np.pi points[..., 0] = points[..., 0] * conversion_factor points[..., 1] = points[..., 1] * conversion_factor return points
def _get_sph(self, convention='top_colat', unit='rad', convert=False): # check if object is empty self._check_empty() x = self._x y = self._y z = self._z # convert to spherical... # ... top polar systems if convention[0:3] == 'top': angles_1, angles_2, radius = cart2sph(x, y, z) if convention == 'top_elev': angles_2 = np.pi / 2 - angles_2 # ... side polar system # (idea for simple conversions from Robert Baumgartner and SOFA_API) elif convention == 'side': angles_2, angles_1, radius = cart2sph(x, z, -y) # range angles angles_1 = angles_1 - np.pi / 2 angles_2 = np.mod(angles_2 + np.pi / 2, 2 * np.pi) - np.pi / 2 # ... front polar system elif convention == 'front': angles_1, angles_2, radius = cart2sph(y, z, x) else: raise ValueError( f"Conversion for {convention} is not implemented.") # convert to degrees if unit == 'deg': warnings.warn(( "'deg' parameter will be deprecated in pyfar 0.8.0 in favor " "of the pyfar.deg2rad and pyfar.rad2deg"), PyfarDeprecationWarning) angles_1 = angles_1 / np.pi * 180 angles_2 = angles_2 / np.pi * 180 elif not unit == 'rad': raise ValueError( f"{unit} is not implemented.") # return points return angles_1, angles_2, radius
[docs] def set_cyl(self, azimuth, z, radius_z, convention='top', unit='rad'): """ This function will be deprecated in pyfar 0.8.0 in favor of the :py:func:`cylindrical` property. For conversions from or into degree use :py:func:`deg2rad` and :py:func:`rad2deg`. Enter coordinate points in cylindrical coordinate systems. The points that enter the Coordinates object are defined by the `domain`, `convention`, and `unit` +--------------------+----------+------------+----------+----------+ | domain, convention | points_1 | points_2 | points_3 | unit | +====================+==========+============+==========+==========+ | cyl, top | azimuth | z | radius_z | rad, deg | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- points_i: array like, number Points for the first, second, and third coordinate convention : string, optional Convention in which the coordinate points are stored. The default is ``'top'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'rad'``. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of the cylindrical property."), PyfarDeprecationWarning) self._set_cyl(azimuth, z, radius_z, convention)
def _set_cyl(self, azimuth, z, rho, convention='top', unit='rad'): # Convert to array azimuth = np.asarray(azimuth) z = np.asarray(z) rho = np.asarray(rho) # squeeze if len(azimuth.shape) == 2 and \ (azimuth.shape[0] == 1 or azimuth.shape[1] == 1): azimuth = azimuth.flatten() if len(z.shape) == 2 and \ (z.shape[0] == 1 or z.shape[1] == 1): z = z.flatten() if len(rho.shape) == 2 and \ (rho.shape[0] == 1 or rho.shape[1] == 1): rho = rho.flatten() # convert to radians if unit == 'deg': warnings.warn(( "'deg' parameter will be deprecated in pyfar 0.8.0 in favor " "of the pyfar.deg2rad and pyfar.rad2deg"), PyfarDeprecationWarning) azimuth = azimuth / 180 * np.pi elif not unit == 'rad': raise ValueError( f"{unit} is not implemented.") # ... from cylindrical coordinate systems if convention == 'top': x, y, z = cyl2cart(azimuth, z, rho) else: # Can not be tested. Will only be raised if a coordinate system # is not fully implemented. raise ValueError( (f"Conversion for {convention} is not implemented.")) # set the coordinate system self._system = self._make_system('cyl', convention, unit) # save coordinates to self self._set_points(x, y, z)
[docs] def get_cyl(self, convention='top', unit='rad', convert=False): """ This function will be deprecated in pyfar 0.8.0 in favor of the `cylindrical` property. For conversions from or into degree use :py:func:`deg2rad` and :py:func:`rad2deg`. Get coordinate points in cylindrical coordinate system. The points that are returned are defined by the `domain`, `convention`, and `unit`: +--------------------+----------+------------+----------+----------+ | domain, convention | p[...,1] | p[...,1] | p[...,1] | units | +====================+==========+============+==========+==========+ | cyl, top | azimuth | z | radius_z | rad, deg | +--------------------+----------+------------+----------+----------+ For more information run >>> coords = Coordinates() >>> coords.systems() Parameters ---------- convention : string, optional Convention in which the coordinate points are stored. The default is ``'right'``. unit : string, optional Unit in which the coordinate points are stored. The default is ``'rad'``. The ``'deg'`` parameter will be deprecated in pyfar 0.8.0 in favor of the :py:func:`deg2rad` and :py:func:`rad2deg`. convert : boolean, optional If True, the internal representation of the samplings points will be converted to the queried coordinate system. The default is False, i.e., the internal presentation remains as it is. Returns ------- points : numpy array Coordinate points. ``points[...,0]`` holds the points for the first coordinate, ``points[...,1]`` the points for the second, and ``points[...,2]`` the points for the third coordinate. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of the cylindrical property."), PyfarDeprecationWarning) points = self.cylindrical conversion_factor = 1 if unit == 'rad' else 180 / np.pi points[..., 0] = points[..., 0] * conversion_factor return points
def _get_cyl(self, convention='top', unit='rad'): """internal function to convert cart to cyl coordinates""" # check if object is empty self._check_empty() # convert to cylindrical ... # ... top systems if convention == 'top': azimuth, z, rho = cart2cyl(self.x, self.y, self.z) else: # Can not be tested. Will only be raised if a coordinate system # is not fully implemented. raise ValueError( f"Conversion for {convention} is not implemented.") # convert to degrees if unit == 'deg': warnings.warn(( "'deg' parameter will be deprecated in pyfar 0.8.0 in favor " "of the pyfar.deg2rad and pyfar.rad2deg"), PyfarDeprecationWarning) azimuth = azimuth / np.pi * 180 elif unit != 'rad': raise ValueError( f"unit for {unit} is not implemented.") # return points and convert internal state if desired return azimuth, z, rho @property def weights(self): """Get sampling weights.""" return self._weights @weights.setter def weights(self, value): """Set sampling weights.""" self._set_weights(value) @property def sh_order(self): """This function will be deprecated in pyfar 0.8.0 in favor of :py:class:`spharpy.samplings.SamplingSphere`. Get the maximum spherical harmonic order.""" warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of spharpy.samplings.SamplingSphere."), PyfarDeprecationWarning) return self._sh_order @sh_order.setter def sh_order(self, value): """This function will be deprecated in pyfar 0.8.0 in favor of :py:class:`spharpy.samplings.SamplingSphere`. Set the maximum spherical harmonic order.""" warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of spharpy.samplings.SamplingSphere."), PyfarDeprecationWarning) self._sh_order = int(value) @property def comment(self): """Get comment.""" return self._comment @comment.setter def comment(self, value): """Set comment.""" if not isinstance(value, str): raise TypeError("comment has to be of type string.") else: self._comment = value @property def cshape(self): """ Return channel shape. The channel shape gives the shape of the coordinate points excluding the last dimension, which is always 3. """ if self._x.size: return self._x.shape else: return (0,) @property def cdim(self): """ Return channel dimension. The channel dimension gives the number of dimensions of the coordinate points excluding the last dimension. """ if self._x.size: return self._x.ndim else: return 0 @property def csize(self): """ Return channel size. The channel size gives the number of points stored in the coordinates object. """ return self._x.size @property def cartesian(self): """ Returns :py:func:`x`, :py:func:`y`, :py:func:`z`. Right handed cartesian coordinate system. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" return np.atleast_2d(np.moveaxis( np.array([self.x, self.y, self.z]), 0, -1)) @cartesian.setter def cartesian(self, value): self._set_points(value[..., 0], value[..., 1], value[..., 2]) @property def spherical_elevation(self): """ Spherical coordinates according to the top pole elevation coordinate system. :py:func:`azimuth`, :py:func:`elevation`, :py:func:`radius`. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" azimuth, elevation, radius = cart2sph(self.x, self.y, self.z) elevation = np.pi / 2 - elevation return np.atleast_2d(np.moveaxis( np.array([azimuth, elevation, radius]), 0, -1)) @spherical_elevation.setter def spherical_elevation(self, value): value[..., 1] = _check_array_limits( value[..., 1], -np.pi/2, np.pi/2, 'elevation angle') x, y, z = sph2cart( value[..., 0], np.pi / 2 - value[..., 1], value[..., 2]) self._set_points(x, y, z) @property def spherical_colatitude(self): """ Spherical coordinates according to the top pole colatitude coordinate system. Returns :py:func:`azimuth`, :py:func:`colatitude`, :py:func:`radius`. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" azimuth, colatitude, radius = cart2sph(self.x, self.y, self.z) return np.atleast_2d(np.moveaxis( np.array([azimuth, colatitude, radius]), 0, -1)) @spherical_colatitude.setter def spherical_colatitude(self, value): value[..., 1] = _check_array_limits( value[..., 1], 0, np.pi, 'colatitude angle') x, y, z = sph2cart(value[..., 0], value[..., 1], value[..., 2]) self._set_points(x, y, z) @property def spherical_side(self): """ Spherical coordinates according to the side pole coordinate system. Returns :py:func:`lateral`, :py:func:`polar`, :py:func:`radius`. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" polar, lateral, radius = cart2sph(self.x, self.z, -self.y) lateral = lateral - np.pi / 2 polar = np.mod(polar + np.pi / 2, 2 * np.pi) - np.pi / 2 return np.atleast_2d(np.moveaxis( np.array([lateral, polar, radius]), 0, -1)) @spherical_side.setter def spherical_side(self, value): value[..., 0] = _check_array_limits( value[..., 0], -np.pi/2, np.pi/2, 'polar angle') x, z, y = sph2cart( value[..., 1], np.pi / 2 - value[..., 0], value[..., 2]) self._set_points(x, y, z) @property def spherical_front(self): """ Spherical coordinates according to the frontal pole coordinate system. Returns :py:func:`frontal`, :py:func:`upper`, :py:func:`radius`. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" frontal, upper, radius = cart2sph(self.y, self.z, self.x) return np.atleast_2d(np.moveaxis( np.array([frontal, upper, radius]), 0, -1)) @spherical_front.setter def spherical_front(self, value): value[..., 1] = _check_array_limits( value[..., 1], 0, np.pi, 'frontal angle') y, z, x = sph2cart(value[..., 0], value[..., 1], value[..., 2]) self._set_points(x, y, z) @property def cylindrical(self): """ Cylindrical coordinates. Returns :py:func:`azimuth`, :py:func:`z`, :py:func:`rho`. See :py:mod:`coordinates concepts <pyfar._concepts.coordinates>` for more information.""" azimuth, z, rho = cart2cyl(self.x, self.y, self.z) return np.atleast_2d(np.moveaxis( np.array([azimuth, z, rho]), 0, -1)) @cylindrical.setter def cylindrical(self, value): x, y, z = cyl2cart(value[..., 0], value[..., 1], value[..., 2]) self._set_points(x, y, z) @property def x(self): r""" X coordinate of a right handed Cartesian coordinate system in meters (-\infty < x < \infty).""" self._check_empty() return self._x @x.setter def x(self, value): self._set_points(value, self.y, self.z) @property def y(self): r""" Y coordinate of a right handed Cartesian coordinate system in meters (-\infty < y < \infty).""" self._check_empty() return self._y @y.setter def y(self, value): self._set_points(self.x, value, self.z) @property def z(self): r""" Z coordinate of a right handed Cartesian coordinate system in meters (-\infty < z < \infty).""" self._check_empty() return self._z @z.setter def z(self, value): self._set_points(self.x, self.y, value) @property def rho(self): r""" Radial distance to the the z-axis of the right handed Cartesian coordinate system in meters (0 \leq radius < \infty).""" return self.cylindrical[..., 2] @rho.setter def rho(self, rho): cylindrical = self.cylindrical cylindrical[..., 2] = rho self.cylindrical = cylindrical @property def radius(self): r""" Radial distance to the origin of the coordinate system in meters (0 \leq radius < \infty).""" return np.sqrt(self.x**2 + self.y**2 + self.z**2) @radius.setter def radius(self, radius): spherical_colatitude = self.spherical_colatitude spherical_colatitude[..., 2] = radius self.spherical_colatitude = spherical_colatitude @property def azimuth(self): r""" Counter clock-wise angle in the x-y plane of the right handed Cartesian coordinate system in radians. 0 radians are defined in positive x-direction, pi/2 radians in positive y-direction and so on (-\infty < azimuth < \infty, 2pi-cyclic).""" return self.spherical_colatitude[..., 0] @azimuth.setter def azimuth(self, azimuth): spherical_colatitude = self.spherical_colatitude spherical_colatitude[..., 0] = azimuth self.spherical_colatitude = spherical_colatitude @property def elevation(self): r""" Angle in the x-z plane of the right handed Cartesian coordinate system in radians. 0 radians elevation are defined in positive x-direction, pi/2 radians in positive z-direction, and -pi/2 in negative z-direction (0 \leq azimuth \leq pi). The elevation is a variation of the colatitude.""" return self.spherical_elevation[..., 1] @elevation.setter def elevation(self, elevation): spherical_elevation = self.spherical_elevation spherical_elevation[..., 1] = elevation self.spherical_elevation = spherical_elevation @property def colatitude(self): r""" Angle in the x-z plane of the right handed Cartesian coordinate system in radians. 0 radians elevation are defined in positive z-direction, pi/2 radians in positive x-direction, and pi in negative z-direction (pi/2 \leq azimuth \leq pi/2). The colatitude is a variation of the elevation angle.""" return self.spherical_colatitude[..., 1] @colatitude.setter def colatitude(self, colatitude): spherical_colatitude = self.spherical_colatitude spherical_colatitude[..., 1] = colatitude self.spherical_colatitude = spherical_colatitude @property def frontal(self): r""" Angle in the y-z plane of the right handed Cartesian coordinate system in radians. 0 radians elevation are defined in positive y-direction, pi/2 radians in positive z-direction, pi in negative y-direction and so on (-\infty < azimuth < \infty, 2pi-cyclic).""" return self.spherical_front[..., 0] @frontal.setter def frontal(self, frontal): spherical_front = self.spherical_front spherical_front[..., 0] = frontal self.spherical_front = spherical_front @property def upper(self): r""" Angle in the x-z plane of the right handed Cartesian coordinate system in radians. 0 radians elevation are defined in positive x-direction, pi/2 radians in positive z-direction, and pi in negative x-direction (0 \leq azimuth \leq pi).""" return self.spherical_front[..., 1] @upper.setter def upper(self, upper): spherical_front = self.spherical_front spherical_front[..., 1] = upper self.spherical_front = spherical_front @property def lateral(self): r""" Counter clock-wise angle in the x-y plane of the right handed Cartesian coordinate system in radians. 0 radians are defined in positive x-direction, pi/2 radians in positive y-direction and -pi/2 in negative y-direction (-pi/2 \leq lateral \leq pi/2).""" return self.spherical_side[..., 0] @lateral.setter def lateral(self, lateral): spherical_side = self.spherical_side spherical_side[..., 0] = lateral self.spherical_side = spherical_side @property def polar(self): r""" Angle in the x-z plane of the right handed Cartesian coordinate system in radians. 0 radians elevation are defined in positive x-direction, pi/2 radians in positive z-direction, pi in negative x-direction and so on (-\infty < azimuth < \infty, 2pi-cyclic).""" return self.spherical_side[..., 1] @polar.setter def polar(self, polar): spherical_side = self.spherical_side spherical_side[..., 1] = polar self.spherical_side = spherical_side
[docs] def systems(self, show='all', brief=False): """ This function will be deprecated in pyfar 0.8.0, check the documentation instead. Print coordinate systems and their description on the console. .. note:: All coordinate systems are described with respect to the right handed cartesian system (``domain='cart'``, ``convention='right'``). Distances are always specified in meters, while angles can be radians or degrees (``unit='rad'`` or ``unit='deg'``). Parameters ---------- show: string, optional ``'current'`` to list the current coordinate system or ``'all'`` to list all coordinate systems. The default is ``'all'``. brief : boolean, optional Will only list the domains, conventions and units if True. The default is ``False``. Returns ------- Prints to console. """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0."), PyfarDeprecationWarning) if show == 'current': domain = self._system['domain'] convention = self._system['convention'] unit = self._system['unit'] elif show == 'all': domain = convention = unit = 'all' else: raise ValueError("show must be 'current' or 'all'.") # get coordinate systems systems = self._systems() # print information domains = list(systems) if domain == 'all' else [domain] if brief: print('domain, convention, unit') print('- - - - - - - - - - - - -') for dd in domains: conventions = list(systems[dd]) if convention == 'all' \ else [convention] for cc in conventions: # current coordinates coords = systems[dd][cc]['coordinates'] # current units if unit != 'all': units = [units for units in systems[dd][cc]['units'] if unit == units[0][0:3]] else: units = systems[dd][cc]['units'] # key for unit unit_key = [u[0][0:3] for u in units] print(f"{dd}, {cc}, [{', '.join(unit_key)}]") else: for dd in domains: conventions = \ list(systems[dd]) if convention == 'all' else [convention] for cc in conventions: # current coordinates coords = systems[dd][cc]['coordinates'] # current units if unit != 'all': units = [units for units in systems[dd][cc]['units'] if unit == units[0][0:3]] else: units = systems[dd][cc]['units'] # key for unit unit_key = [u[0][0:3] for u in units] print("- - - - - - - - - - - - - - - - - " "- - - - - - - - - - - - - - - - -") print(f"domain: {dd}, convention: {cc}, unit: " f"[{', '.join(unit_key)}]\n") print(systems[dd][cc]['description_short'] + '\n') print("Coordinates:") for nn, coord in enumerate(coords): cur_units = [u[nn] for u in units] print( f"points_{nn + 1}: {coord} ", f"[{', '.join(cur_units)}]") print('\n' + systems[dd][cc]['description'] + '\n\n')
[docs] def show(self, mask=None, **kwargs): """ Show a scatter plot of the coordinate points. Parameters ---------- mask : boolean numpy array, None, optional Mask or indexes to highlight. Highlight points in red if ``mask==True``. The default is ``None``, which plots all points in the same color. kwargs : optional Keyword arguments are passed to ``matplotlib.pyplot.scatter()``. If a mask is provided and the key `c` is contained in kwargs, it will be overwritten. Returns ------- ax : matplotlib.axes._subplots.Axes3DSubplot The axis used for the plot. """ if mask is None: ax = pf.plot.scatter(self, **kwargs) else: mask = np.asarray(mask) colors = np.full(self.cshape, pf.plot.color('b')) colors[mask] = pf.plot.color('r') ax = pf.plot.scatter(self, c=colors.flatten(), **kwargs) ax.set_box_aspect([ np.ptp(self.x), np.ptp(self.y), np.ptp(self.z)]) ax.set_aspect('equal') return ax
[docs] def find_nearest(self, find, k=1, distance_measure='euclidean'): """ Find the k nearest coordinates points. Parameters ---------- find : pf.Coordinates Coordinates to which the nearest neighbors are searched. k : int, optional Number of points to return. k must be > 0. The default is ``1``. distance_measure : string, optional ``'euclidean'`` distance is determined by the euclidean distance. This is default. ``'spherical_radians'`` distance is determined by the great-circle distance expressed in radians. ``'spherical_meter'`` distance is determined by the great-circle distance expressed in meters. Returns ------- index : tuple of arrays Indices of the neighbors. Arrays of shape ``(k, find.cshape)`` if k>1 else ``(find.cshape, )``. distance : numpy array of floats Distance between the points, after the given ``distance_measure``. It's of shape (k, find.cshape). Notes ----- This is a wrapper for ``scipy.spatial.cKDTree``. Examples -------- Find frontal point from a spherical coordinate system .. plot:: >>> import pyfar as pf >>> coords = pf.samplings.sph_lebedev(sh_order=10) >>> to_find = pf.Coordinates(1, 0, 0) >>> index, distance = coords.find_nearest(to_find) >>> coords.show(index) >>> distance 0.0 Find multidimensional points in multidimensional coordinates with k=1 >>> import pyfar as pf >>> import numpy as np >>> coords = pf.Coordinates(np.arange(9).reshape((3, 3)), 0, 1) >>> to_find = pf.Coordinates( >>> np.array([[0, 1], [2, 3]]), 0, 1) >>> i, d = coords.find_nearest(to_find) >>> coords[i] == find True >>> i (array([[0, 0], [0, 1]], dtype=int64), array([[0, 1], [2, 0]], dtype=int64)) >>> d array([[0., 0.], [0., 0.]]) Find multidimensional points in multidimensional coordinates with k=3 >>> import pyfar as pf >>> import numpy as np >>> coords = pf.Coordinates(np.arange(9).reshape((3, 3)), 0, 1) >>> find = pf.Coordinates( >>> np.array([[0, 1], [2, 3]]), 0, 1) >>> i, d = coords.find_nearest(find, 3) >>> # the k-th dimension is at the end >>> i[0].shape (3, 2, 2) >>> # now just access the k=0 dimension >>> coords[i][0].cartesian array([[[0., 0., 1.], [1., 0., 1.]], [[2., 0., 1.], [3., 0., 1.]]]) """ # check the input if not isinstance(k, int) or k <= 0 or k > self.csize: raise ValueError("k must be an integer > 0 and <= self.csize.") if not isinstance(find, Coordinates): raise ValueError("find must be an pf.Coordinates object.") allowed_measures = [ 'euclidean', 'spherical_radians', 'spherical_meter'] if distance_measure not in allowed_measures: raise ValueError( f"distance_measure needs to be in {allowed_measures} and " f"it is {distance_measure}") # get target point in cartesian coordinates points = find.cartesian # get KDTree kdtree = self._make_kdtree() # query nearest neighbors points = points.flatten() if find.csize == 1 else points # nearest points distance, index = kdtree.query(points, k=k) if distance_measure in ['spherical_radians', 'spherical_meter']: # determine validate radius radius = np.concatenate((self.radius, find.radius)) delta_radius = np.max(radius) - np.min(radius) if delta_radius > 1e-15: raise ValueError( "find_nearest_sph only works if all points have the same \ radius. Differences are larger than 1e-15") radius = np.max(radius) # convert cartesian coordinates to length on the great circle using # the Haversine formula distance = 2 * np.arcsin(distance / (2 * radius)) if distance_measure == 'spherical_meter': # convert angle in radiant to distance on the sphere # distance = 2*radius*pi*distance/(2*pi) = radius*distance distance *= radius if self.cdim == 1: if k > 1: index_multi = np.moveaxis(index, -1, 0) index = np.empty((k), dtype=tuple) for kk in range(k): index[kk] = tuple([index_multi[kk]], ) else: index = tuple([index], ) else: index_array = np.arange(self.csize).reshape(self.cshape) index_multi = [] for dim in range(self.cdim): index_multi.append([]) for i in index.flatten(): index_multi[dim].append(np.where(i == index_array)[dim][0]) index_multi[dim] = np.asarray( index_multi[dim]).reshape(index.shape) if k > 1: index_multi = np.moveaxis(index_multi, -1, 0) index = np.empty((k), dtype=tuple) for kk in range(k): index[kk] = tuple(index_multi[kk]) else: index = tuple(index_multi) if k > 1: distance = np.moveaxis(distance, -1, 0) return index, distance
[docs] def find_within( self, find, distance=0., distance_measure='euclidean', atol=None, return_sorted=True, radius_tol=None): """ Find coordinates within a certain distance to the query points. Parameters ---------- find : pf.Coordinates Coordinates to which the nearest neighbors are searched. distance : number, optional Maximum allowed distance to the given points ``find``. Distance must be >= 0. For just exact matches use ``0``. The default is ``0``. distance_measure : string, optional ``'euclidean'`` distance is determined by the euclidean distance. This is default. ``'spherical_radians'`` distance is determined by the great-circle distance expressed in radians. ``'spherical_meter'`` distance is determined by the great-circle distance expressed in meters. atol : float, None Absolute tolerance for distance. The default ``None`` uses a tolerance of two times the decimal resolution, which is determined from the data type of the coordinate points using ``numpy.finfo``. return_sorted : bool, optional Sorts returned indices if True and does not sort them if False. The default is True. radius_tol : float, None For all spherical distance measures, the coordinates must be on a sphere, so the radius must be constant. This parameter defines the maximum allowed difference within the radii. Note that increasing the tolerance decreases the accuracy of the search, i.e., points that are within the search distance might not be found or points outside the search distance may be returned. The default ``None`` uses a tolerance of two times the decimal resolution, which is determined from the data type of the coordinate points using ``numpy.finfo``. Returns ------- index : tuple of array Indices of the containing coordinates. Arrays of shape (find.cshape). Notes ----- This is a wrapper for ``scipy.spatial.cKDTree``. Compared to previous implementations, it supports self.ndim>1 as well. Examples -------- Find all point with 1m distance from the frontal point .. plot:: >>> import pyfar as pf >>> coords = pf.samplings.sph_lebedev(sh_order=10) >>> find = pf.Coordinates(1, 0, 0) >>> index = coords.find_within(find, 1) >>> coords.show(index) Find all point with 1m distance from two points .. plot:: >>> import pyfar as pf >>> coords = pf.Coordinates(np.arange(6), 0, 0) >>> find = pf.Coordinates([2, 3], 0, 0) >>> index = coords.find_within(find, 1) >>> coords.show(index[0]) """ # check the input if radius_tol is None: radius_tol = 2 * np.finfo(self.x.dtype).resolution if atol is None: atol = 2 * np.finfo(self.x.dtype).resolution if float(distance) < 0: raise ValueError("distance must be a non negative number.") if not isinstance(atol, float) or atol < 0: raise ValueError("atol must be a non negative number.") if not isinstance(radius_tol, float) or radius_tol < 0: raise ValueError("radius_tol must be a non negative number.") if not isinstance(find, Coordinates): raise ValueError("coords must be an pf.Coordinates object.") if not isinstance(return_sorted, bool): raise ValueError("return_sorted must be a bool.") allowed_measures = [ 'euclidean', 'spherical_radians', 'spherical_meter'] if distance_measure not in allowed_measures: raise ValueError( f"distance_measure needs to be in {allowed_measures} and " f"it is {distance_measure}") # get target point in cartesian coordinates points = find.cartesian # get KDTree kdtree = self._make_kdtree() # query nearest neighbors points = points.flatten() if find.csize == 1 else points # nearest points if distance_measure == 'euclidean': index = kdtree.query_ball_point( points, distance + atol, return_sorted=return_sorted) if distance_measure in ['spherical_radians', 'spherical_meter']: # determine validate radius radius = self.radius delta_radius = np.max(radius) - np.min(radius) if delta_radius > radius_tol: raise ValueError( "find_nearest_sph only works if all points have the same " f"radius. Differences are larger than {radius_tol}") radius = np.max(radius) if distance_measure == 'spherical_meter': # convert angle in radiant to distance on the sphere # d = 2r*pi*d/(2*pi) = r*d distance = radius * distance # convert length on the great circle to in cartesian coordinates distance = 2 * radius * np.sin(distance / (2 * radius)) index = kdtree.query_ball_point( points, distance + atol, return_sorted=return_sorted) if self.cdim == 1: if find.csize > 1: for i in range(len(index)): index[i] = tuple([index[i]], ) else: index = tuple([index], ) else: index_array = np.arange(self.csize).reshape(self.cshape) index_new = np.empty((find.csize), dtype=tuple) for i in range(find.csize): index_multi = [] if find.csize > 1: for j in index[i]: index_multi.append(np.where(j == index_array)) else: for j in index: index_multi.append(np.where(j == index_array)) index_multi = np.moveaxis(np.squeeze( np.asarray(index_multi)), -1, 0) if find.csize > 1: index_new[i] = tuple(index_multi) else: index_new = tuple(index_multi) index = index_new return index
[docs] def find_nearest_k(self, points_1, points_2, points_3, k=1, domain='cart', convention='right', unit='met', show=False): """ This function will be deprecated in pyfar 0.8.0 in favor of the ``find_nearest`` method. Find the k nearest coordinates points. Parameters ---------- points_i : array like, number First, second and third coordinate of the points to which the nearest neighbors are searched. k : int, optional Number of points to return. k must be > 0. The default is ``1``. domain : string, optional Domain of the points. The default is ``'cart'``. convention: string, optional Convention of points. The default is ``'right'``. unit : string, optional Unit of the points. The default is ``'met'`` for meters. show : bool, optional Show a plot of the coordinate points. The default is ``False``. Returns ------- index : numpy array of ints The locations of the neighbors in the getter methods (e.g., ``self.cartesian``). Dimension according to `distance` (see below). Missing neighbors are indicated with ``csize``. Also see Notes below. mask : boolean numpy array Mask that contains ``True`` at the positions of the selected points and ``False`` otherwise. Mask is of shape ``cshape``. Notes ----- ``numpy.spatial.cKDTree`` is used for the search, which requires an (N, 3) array. The coordinate points in self are thus reshaped to (`csize`, 3) before they are passed to ``cKDTree``. The index that is returned refers to the reshaped coordinate points. To access the points for example use >>> points_reshaped = self.cartesian.reshape((self.csize, 3)) >>> points_reshaped[index] Examples -------- Find the nearest point in a line .. plot:: >>> import pyfar as pf >>> coords = pf.Coordinates(np.arange(-5, 5), 0, 0) >>> result = coords.find_nearest_k(0, 0, 0, show=True) """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of find_nearest method."), PyfarDeprecationWarning) # check the input assert isinstance(k, int) and k > 0 and k <= self.csize, \ "k must be an integer > 0 and <= self.csize." # get the points _, index, mask = self._find_nearest( points_1, points_2, points_3, domain, convention, unit, show, k, 'k') return index, mask
[docs] def find_nearest_cart(self, points_1, points_2, points_3, distance, domain='cart', convention='right', unit='met', show=False, atol=1e-15): """ This function will be deprecated in pyfar 0.8.0 in favor of the ``find_within`` method. Find coordinates within a certain distance in meters to query points. Parameters ---------- points_i : array like, number First, second and third coordinate of the points to which the nearest neighbors are searched. distance : number Euclidean distance in meters in which the nearest points are searched. Must be >= 0. domain : string, optional Domain of the points. The default is ``'cart'``. convention: string, optional Convention of points. The default is ``'right'``. unit : string, optional Unit of the points. The default is ``'met'`` for meters. show : bool, optional Show a plot of the coordinate points. The default is ``False``. atol : float, optional A tolerance that is added to `distance`. The default is`` 1e-15``. Returns ------- index : numpy array of ints The locations of the neighbors in the getter methods (e.g., ``cartesian``). Dimension as in :py:func:`~find_nearest_k`. Missing neighbors are indicated with ``csize``. Also see Notes below. mask : boolean numpy array Mask that contains ``True`` at the positions of the selected points and ``False`` otherwise. Mask is of shape ``cshape``. Notes ----- ``numpy.spatial.cKDTree`` is used for the search, which requires an (N, 3) array. The coordinate points in self are thus reshaped to (`csize`, 3) before they are passed to ``cKDTree``. The index that is returned refers to the reshaped coordinate points. To access the points for example use >>> points_reshaped = self.cartesian.reshape((self.csize, 3)) >>> points_reshaped[index] Examples -------- Find frontal points within a distance of 0.5 meters .. plot:: >>> import pyfar as pf >>> coords = pf.Coordinates(np.arange(-5, 5), 0, 0) >>> result = coords.find_nearest_cart(2, 0, 0, 0.5, show=True) """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of find_within method."), PyfarDeprecationWarning) # check the input assert distance >= 0, "distance must be >= 0" # get the points distance, index, mask = self._find_nearest( points_1, points_2, points_3, domain, convention, unit, show, distance, 'cart', atol) return index, mask
[docs] def find_nearest_sph(self, points_1, points_2, points_3, distance, domain='sph', convention='top_colat', unit='rad', show=False, atol=1e-15): """ This function will be deprecated in pyfar 0.8.0 in favor of the ``find_within`` method. Find coordinates within certain angular distance to the query points. Parameters ---------- points_i : array like, number First, second and third coordinate of the points to which the nearest neighbors are searched. distance : number Great circle distance in degrees in which the nearest points are searched. Must be >= 0 and <= 180. domain : string, optional Domain of the input points. The default is ``'sph'``. convention: string, optional Convention of the input points. The default is ``'top_colat'``. unit: string, optional Unit of the input points. The default is ``'rad'``. show : bool, optional Show a plot of the coordinate points. The default is ``False``. atol : float, optional A tolerance that is added to `distance`. The default is ``1e-15``. Returns ------- index : numpy array of ints The locations of the neighbors in the getter methods (e.g., ``cartesian``). Dimension as in :py:func:`~find_nearest_k`. Missing neighbors are indicated with ``csize``. Also see Notes below. mask : boolean numpy array Mask that contains ``True`` at the positions of the selected points and ``False`` otherwise. Mask is of shape ``cshape``. Notes ----- ``numpy.spatial.cKDTree`` is used for the search, which requires an (N, 3) array. The coordinate points in self are thus reshaped to (`csize`, 3) before they are passed to ``cKDTree``. The index that is returned refers to the reshaped coordinate points. To access the points for example use ``points_reshaped = points.cartesian.reshape((points.csize, 3))`` ``points_reshaped[index]`` Examples -------- Find top points within a distance of 45 degrees .. plot:: >>> import pyfar as pf >>> import numpy as np >>> coords = pf.Coordinates.from_spherical_elevation( >>> 0, np.arange(-90, 91, 10)*np.pi/180, 1) >>> result = coords.find_nearest_sph(0, np.pi/2, 1, 45, show=True) """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0 in favor " "of find_within method."), PyfarDeprecationWarning) # check the input assert distance >= 0 and distance <= 180, \ "distance must be >= 0 and <= 180." # get radius and check for equality radius = self.radius delta_radius = np.max(radius) - np.min(radius) if delta_radius > 1e-15: raise ValueError( "find_nearest_sph only works if all points have the same \ radius. Differences are larger than 1e-15") # get the points distance, index, mask = self._find_nearest( points_1, points_2, points_3, domain, convention, unit, show, distance, 'sph', atol, np.max(radius)) return index, mask
[docs] def find_slice(self, coordinate: str, unit: str, value, tol=0, show=False, atol=1e-15): """ This function will be deprecated in pyfar 0.8.0. Use properties and slicing instead, e.g. ``coords = coords[coords.azimuth>=np.pi]``. Find a slice of the coordinates points. Parameters ---------- coordinate : str Coordinate for slicing. unit : str Unit in which the value is passed value : number Value of the coordinate around which the points are sliced. tol : number, optional Tolerance for slicing. Points are sliced within the range ``[value-tol, value+tol]``. The default is ``0``. show : bool, optional Show a plot of the coordinate points. The default is ``False``. atol : number, optional A tolerance that is added to `tol`. The default is ``1e-15``. Returns ------- index : tuple of numpy arrays The indices of the selected points as a tuple of arrays. The length of the tuple matches :py:func:`~cdim`. The length of each array matches the number of selected points. mask : boolean numpy array Mask that contains True at the positions of the selected points and False otherwise. Mask is of shape self.cshape. Notes ----- `value` must be inside the range of the coordinate (see ``.systems``). However, `value` +/- `tol` may exceed the range. Examples -------- Find horizontal slice of spherical coordinate system within a ring of +/- 10 degrees .. plot:: >>> import pyfar as pf >>> import numpy as np >>> coords = pf.Coordinates.from_spherical_elevation( >>> np.arange(-30, 30, 5)*np.pi/180, 0, 1) >>> result = coords.find_slice('azimuth', 'deg', 0, 5, show=True) """ warnings.warn(( "This function will be deprecated in pyfar 0.8.0. Use properties" " and slicing instead."), PyfarDeprecationWarning) # check if the coordinate and unit exist domain, convention, index = self._exist_coordinate(coordinate, unit) # get type and range of coordinate c_info = self._systems()[domain][convention][coordinate] # convert input to radians value = value / 180 * np.pi if unit == 'deg' else value tol = tol / 180 * np.pi if unit == 'deg' else tol # check if value is within the range of coordinate if c_info[0] in ["bound", "cyclic"]: assert c_info[1][0] <= value <= c_info[1][1], \ f"'value' is {value} but must be in the range {c_info[1]}." # get the search range rng = [value - tol, value + tol] # wrap range if coordinate is cyclic if c_info[0] == 'cyclic': low = c_info[1][0] upp = c_info[1][1] if rng[0] < c_info[1][0] - atol: rng[0] = (rng[0] - low) % (upp - low) + low if rng[1] > c_info[1][1] + atol: rng[1] = (rng[1] - low) % (upp - low) + low # get the coordinates coords = eval(f"self.{coordinate}") # get the mask if rng[0] <= rng[1]: mask = (coords >= rng[0] - atol) & (coords <= rng[1] + atol) else: mask = (coords >= rng[0] - atol) | (coords <= rng[1] + atol) # plot all and returned points if show: self.show(mask) index = np.where(mask) return index, mask
[docs] def rotate(self, rotation: str, value=None, degrees=True, inverse=False): """ Rotate points stored in the object around the origin of coordinates. This is a wrapper for ``scipy.spatial.transform.Rotation`` (see this class for more detailed information). Parameters ---------- rotation : str ``'quat'`` Rotation given by quaternions. ``'matrix'`` Rotation given by matrixes. ``'rotvec'`` Rotation using rotation vectors. ``'xyz'`` Rotation using euler angles. Up to three letters. E.g., ``'x'`` will rotate about the x-axis only, while ``'xz'`` will rotate about the x-axis and then about the z-axis. Use lower letters for extrinsic rotations (rotations about the axes of the original coordinate system xyz, which remains motionless) and upper letters for intrinsic rotations (rotations about the axes of the rotating coordinate system XYZ, solidary with the moving body, which changes its orientation after each elemental rotation). value : number, array like Amount of rotation in the format specified by `rotation` (see above). degrees : bool, optional Pass angles in degrees if using ``'rotvec'`` or euler angles (``'xyz'``). The default is ``True``. Use False to pass angles in radians. inverse : bool, optional Apply inverse rotation. The default is ``False``. Notes ----- Points are converted to the cartesian right handed coordinate system for the rotation. Examples -------- Get a coordinates object >>> import pyfar as pf >>> coords = pf.Coordinates(np.arange(-5, 5), 0, 0) Rotate 45 degrees about the y-axis using 1. quaternions >>> coordinates.rotate('quat', [0 , 0.38268343, 0 , 0.92387953]) 2. a rotation matrix >>> coordinates.rotate('matrix', ... [[ 0.70710678, 0 , 0.70710678], ... [ 0 , 1 , 0. ], ... [-0.70710678, 0 , 0.70710678]]) 3. a rotation vector >>> coordinates.rotate('rotvec', [0, 45, 0]) 4. euler angles >>> coordinates.rotate('XYZ', [0, 45, 0]) To see the result of the rotation use >>> coordinates.show() """ # initialize rotation object if rotation == 'quat': rot = sp_rot.from_quat(value) elif rotation == 'matrix': rot = sp_rot.from_matrix(value) elif rotation == 'rotvec': if degrees: value = np.asarray(value) / 180 * np.pi rot = sp_rot.from_rotvec(value) elif not bool(re.search('[^x-z]', rotation.lower())): # only check if string contains xyz, everything else is checked in # from_euler() rot = sp_rot.from_euler(rotation, value, degrees) else: raise ValueError("rotation must be 'quat', 'matrix', 'rotvec', " "or from ['x', 'y', 'z'] or ['X', 'Y', 'Z'] but " f"is '{rotation}'") # current shape shape = self.cshape # apply rotation points = rot.apply(self.cartesian.reshape((self.csize, 3)), inverse) # set points self._set_points( points[:, 0].reshape(shape), points[:, 1].reshape(shape), points[:, 2].reshape(shape))
[docs] def copy(self): """Return a deep copy of the Coordinates object.""" return deepcopy(self)
def _encode(self): """Return dictionary for the encoding.""" return self.copy().__dict__ @classmethod def _decode(cls, obj_dict): """Decode object based on its respective ``_encode`` counterpart.""" obj = cls() obj.__dict__.update(obj_dict) return obj @staticmethod def _systems(): """ Get class internal information about all coordinate systems. Returns ------- _systems : nested dictionary List all available coordinate systems. Key 0 - domain, e.g., 'cart' Key 1 - convention, e.g., 'right' Key 2a - 'short_description': string Key 2b - 'coordinates': ['coordinate_1', 'coordinate_2', 'coordinate_3'] Key 2c - 'units': [['unit_1.1','unit_2.1','unit_3.1'], ... ['unit_1.N','unit_2.N','unit_3.N']] Key 2d - 'description' Key 2e - 'front': positive x (for debugging, meters and radians) Key 2f - 'left' : positive y (for debugging, meters and radians) Key 2g - 'back' : negative x (for debugging, meters and radians) Key 2h - 'right': negative y (for debugging, meters and radians) Key 2i - 'up' : positive z (for debugging, meters and radians) Key 2j - 'down' : negative z (for debugging, meters and radians) Key 2k,l,m - coordinate_1,2,3 : [type, [lower_lim, upper_lim]] type can be 'unbound', 'bound', or 'cyclic' """ # define coordinate systems _systems = { "cart": { "right": { "description_short": "Right handed cartesian coordinate system.", "coordinates": ["x", "y", "z"], "units": [["meters", "meters", "meters"]], "description": "Right handed cartesian coordinate system with x,y, " "and z in meters.", "positive_x": [1, 0, 0], "positive_y": [0, 1, 0], "negative_x": [-1, 0, 0], "negative_y": [0, -1, 0], "positive_z": [0, 0, 1], "negative_z": [0, 0, -1], "x": ["unbound", [-np.inf, np.inf]], "y": ["unbound", [-np.inf, np.inf]], "z": ["unbound", [-np.inf, np.inf]]} }, "sph": { "top_colat": { "description_short": "Spherical coordinate system with North and South " "Pole.", "coordinates": ["azimuth", "colatitude", "radius"], "units": [["radians", "radians", "meters"], ["degrees", "degrees", "meters"]], "description": "The azimuth denotes the counter clockwise angle in " "the x/y-plane with 0 pointing in positive x-" "direction and pi/2 in positive y-direction. The " "colatitude denotes the angle downwards from the z-" "axis with 0 pointing in positive z-direction and pi " "in negative z-direction. The azimuth and colatitude " "can be in radians or degrees, the radius is always " "in meters.", "positive_x": [0, np.pi / 2, 1], "positive_y": [np.pi / 2, np.pi / 2, 1], "negative_x": [np.pi, np.pi / 2, 1], "negative_y": [3 * np.pi / 2, np.pi / 2, 1], "positive_z": [0, 0, 1], "negative_z": [0, np.pi, 1], "azimuth": ["cyclic", [0, 2 * np.pi]], "colatitude": ["bound", [0, np.pi]], "radius": ["bound", [0, np.inf]]}, "top_elev": { "description_short": "Spherical coordinate system with North and South " "Pole. Conform with AES69-2015: AES standard for file " "exchange - Spatial acoustic data file format (SOFA).", "coordinates": ["azimuth", "elevation", "radius"], "units": [["radians", "radians", "meters"], ["degrees", "degrees", "meters"]], "description": "The azimuth denotes the counter clockwise angle in " "the x/y-plane with 0 pointing in positive x-" "direction and pi/2 in positive y-direction. The " "elevation denotes the angle upwards and downwards " "from the x/y-plane with pi/2 pointing at positive " "z-direction and -pi/2 pointing in negative z-" "direction. The azimuth and elevation can be in " "radians or degrees, the radius is always in meters.", "positive_x": [0, 0, 1], "positive_y": [np.pi / 2, 0, 1], "negative_x": [np.pi, 0, 1], "negative_y": [3 * np.pi / 2, 0, 1], "positive_z": [0, np.pi / 2, 1], "negative_z": [0, -np.pi / 2, 1], "azimuth": ["cyclic", [0, 2 * np.pi]], "elevation": ["bound", [-np.pi / 2, np.pi / 2]], "radius": ["bound", [0, np.inf]]}, "side": { "description_short": "Spherical coordinate system with poles on the " "y-axis.", "coordinates": ["lateral", "polar", "radius"], "units": [["radians", "radians", "meters"], ["degrees", "degrees", "meters"]], "description": "The lateral angle denotes the angle in the x/y-plane " "with pi/2 pointing in positive y-direction and -pi/2 " "in negative y-direction. The polar angle denotes the " "angle in the x/z-plane with -pi/2 pointing in " "negative z-direction, 0 in positive x-direction, " "pi/2 in positive z-direction, pi in negative x-" "direction. The polar and lateral angle can be in " "radians and degree, the radius is always in meters.", "positive_x": [0, 0, 1], "positive_y": [np.pi / 2, 0, 1], "negative_x": [0, np.pi, 1], "negative_y": [-np.pi / 2, 0, 1], "positive_z": [0, np.pi / 2, 1], "negative_z": [0, -np.pi / 2, 1], "lateral": ["bound", [-np.pi / 2, np.pi / 2]], "polar": ["cyclic", [-np.pi / 2, np.pi * 3 / 2]], "radius": ["bound", [0, np.inf]]}, "front": { "description_short": "Spherical coordinate system with poles on the x-axis." " Conform with AES56-2008 (r2019): AES standard on " "acoustics - Sound source modeling.", "coordinates": ["phi", "theta", "radius"], "units": [["radians", "radians", "meters"], ["degrees", "degrees", "meters"]], "description": "Phi denotes the angle in the y/z-plane with 0 " "pointing in positive y-direction, pi/2 in positive " "z-direction, pi in negative y-direction, and 3*pi/2 " "in negative z-direction. Theta denotes the angle " "measured from the x-axis with 0 pointing in positive " "x-direction and pi in negative x-direction. Phi and " "theta can be in radians and degrees, the radius is " "always in meters.", "positive_x": [0, 0, 1], "positive_y": [0, np.pi / 2, 1], "negative_x": [0, np.pi, 1], "negative_y": [np.pi, np.pi / 2, 1], "positive_z": [np.pi / 2, np.pi / 2, 1], "negative_z": [3 * np.pi / 2, np.pi / 2, 1], "phi": ["cyclic", [0, 2 * np.pi]], "theta": ["bound", [0, np.pi]], "radius": ["bound", [0, np.inf]]} }, "cyl": { "top": { "description_short": "Cylindrical coordinate system along the z-axis.", "coordinates": ["azimuth", "z", "radius_z"], "units": [["radians", "meters", "meters"], ["degrees", "meters", "meters"]], "description": "The azimuth denotes the counter clockwise angle in " "the x/y-plane with 0 pointing in positive x-" "direction and pi/2 in positive y-direction. The " "height is given by z, and radius_z denotes the " "radius measured orthogonal to the z-axis.", "positive_x": [0, 0, 1], "positive_y": [np.pi / 2, 0, 1], "negative_x": [np.pi, 0, 1], "negative_y": [3 * np.pi / 2, 0, 1], "positive_z": [0, 1, 0], "negative_z": [0, -1, 0], "azimuth": ["cyclic", [0, 2 * np.pi]], "z": ["unbound", [-np.inf, np.inf]], "radius_z": ["bound", [0, np.inf]]} } } return _systems def _exist_system(self, domain=None, convention=None, unit=None): """ Check if a coordinate system exists and throw an error if it does not. The coordinate systems are defined in self._systems. Parameters ---------- domain : string Specify the domain of the coordinate system, e.g., 'cart'. convention : string The convention of the coordinate system, e.g., 'top_colat' units: string The unit of the coordinate system (rad, deg, or met for radians, degrees, or meters) """ if domain is None: raise ValueError('The domain must be specified') # get available coordinate systems systems = self._systems() # check if domain exists assert domain in systems or domain is None, \ f"{domain} does not exist. Domain must be one of the following: "\ f"{', '.join(list(systems))}." # check if convention exists in domain if convention is not None: assert convention in systems[domain] or convention is None, \ f"{convention} does not exist in {domain}. Convention must "\ f"be one of the following: {', '.join(list(systems[domain]))}." # check if units exist if unit is not None: # get first convention in domain # (units are the same within a domain) if convention is None: convention = list(systems[domain])[0] cur_units = [u[0][0:3] for u in systems[domain][convention]['units']] assert unit in cur_units, \ f"{unit} does not exist in {domain} convention "\ f"Unit must be one of the following: {', '.join(cur_units)}." def _exist_coordinate(self, coordinate, unit): """ Check if coordinate and unit exist. Returns domain and convention, and the index of coordinate if coordinate and unit exists and raises a value error otherwise. """ # get all systems systems = self._systems() # find coordinate and unit in systems for domain in systems: for convention in systems[domain]: if coordinate in systems[domain][convention]['coordinates']: # get position of coordinate index = systems[domain][convention]['coordinates'].\ index(coordinate) # get possible units units = [u[index][0:3] for u in systems[domain][convention]['units']] # return or raise ValueError if unit in units: return domain, convention, index raise ValueError( (f"'{coordinate}' in '{unit}' does not exist. See " "self.systems() for a list of possible " "coordinates and units")) def _make_system(self, domain=None, convention=None, unit=None): """ Make and return class internal information about coordinate system. """ # check if coordinate system exists self._exist_system(domain, convention, unit) # get the new system system = self._systems() if convention is None: convention = list(system[domain])[0] system = system[domain][convention] # get the units if unit is not None: units = [units for units in system['units'] if unit == units[0][0:3]] units = units[0] else: units = system['units'][0] unit = units[0][0:3] # add class internal keys system['domain'] = domain system['convention'] = convention system['unit'] = unit system['units'] = units return system def _set_points(self, x, y, z): """ Check points and convert to matrix. Parameters ---------- convert : boolean, optional Set self._points if convert = True. Return points as matrix otherwise. The default is False. system: dict, optional The coordinate system against which the range of the points are checked as returned from self._make_system. If system = None self._system is used. Set self._points, which is an atleast_2d numpy array of shape [L,M,...,N, 3]. """ # cast to numpy array x = np.atleast_1d(np.asarray(x, dtype=np.float64)) y = np.atleast_1d(np.asarray(y, dtype=np.float64)) z = np.atleast_1d(np.asarray(z, dtype=np.float64)) # shapes of non scalar entries shapes = [p.shape for p in [x, y, z] if p.ndim != 1 or p.shape[0] > 1] # repeat scalar entries if non-scalars exists if len(shapes): if x.size == 1: x = np.tile(x, shapes[0]) if y.size == 1: y = np.tile(y, shapes[0]) if z.size == 1: z = np.tile(z, shapes[0]) # check for equal shape assert (x.shape == y.shape) and (x.shape == z.shape), \ "x, y, and z must be scalar or of the \ same shape." # set values self._x = x self._y = y self._z = z def _set_weights(self, weights): """ Check and set sampling weights. Set self._weights, which is an atleast_1d numpy array of shape [L,M,...,N]. """ # check input if weights is None: self._weights = weights return # cast to np.array weights = np.asarray(weights, dtype=np.float64) # reshape according to self._points assert weights.size == self.csize, \ "weights must have same size as self.csize" weights = weights.reshape(self.cshape) # set class variable self._weights = weights def _find_nearest(self, points_1, points_2, points_3, domain, convention, unit, show, value, measure, atol=1e-15, radius=None): # get KDTree kdtree = self._make_kdtree() # get target point in cartesian coordinates coords = Coordinates(points_1, points_2, points_3, domain, convention, unit) points = coords.cartesian # query nearest neighbors points = points.flatten() if coords.csize == 1 else points # get the points depending on measure and value if measure == 'k': # nearest points distance, index = kdtree.query(points, k=value) elif measure == 'cart': # points within euclidean distance index = kdtree.query_ball_point(points, value + atol) distance = None elif measure == 'sph': # get radius and check for equality radius = self.radius delta_radius = np.max(radius) - np.min(radius) if delta_radius > 1e-15: raise ValueError( "find_nearest_sph only works if all points have the same \ radius. Differences are larger than 1e-15") radius = np.max(radius) # convert great circle to euclidean distance x, y, z = sph2cart([0, value / 180 * np.pi], [np.pi / 2, np.pi / 2], [radius, radius]) value = np.sqrt((x[0] - x[1])**2 + (y[0] - y[1])**2 + (z[0] - z[1])**2) # points within great circle distance index = kdtree.query_ball_point(points, value + atol) distance = None # mask for scatter plot mask = np.full((self.csize), False) mask[index] = True mask = mask.reshape(self.cshape) # plot all and returned points if show: self.show(mask) return distance, index, mask def _make_kdtree(self): """Make a numpy KDTree for fast search of nearest points.""" xyz = self.cartesian kdtree = cKDTree(xyz.reshape((self.csize, 3))) return kdtree def __getitem__(self, index): """Return copied slice of Coordinates object at index.""" new = self.copy() # slice points new._x = np.atleast_1d(new._x[index]) new._y = np.atleast_1d(new._y[index]) new._z = np.atleast_1d(new._z[index]) # slice weights if new._weights is not None: new._weights = new._weights[index] return new def __array__(self): """Instances of Coordinates behave like `numpy.ndarray`, array_like.""" # copy to avoid changing the coordinate system of the original object return self.copy().cartesian def __repr__(self): """Get info about Coordinates object.""" # object type if self.cshape != (0,): obj = f"{self.cdim}D Coordinates object with {self.csize} points "\ f"of cshape {self.cshape}" else: obj = "Empty Coordinates object" # join information _repr = obj + "\n" # check for sampling weights if self._weights is None: _repr += "\nDoes not contain sampling weights" else: _repr += "\nContains sampling weights" # check for sh_order if self._sh_order is not None: _repr += f"\nSpherical harmonic order: {self._sh_order}" # check for comment if self._comment != "": _repr += f"\nComment: {self._comment}" return _repr def __eq__(self, other): """Check for equality of two objects.""" # return not deepdiff.DeepDiff(self, other) if self.cshape != other.cshape: return False eq_x = self._x == other._x eq_y = self._y == other._y eq_z = self._z == other._z eq_weights = self._weights == other._weights eq_sh_order = self._sh_order == other._sh_order eq_comment = self._comment == other._comment eq_system = self._system == other._system if self._x.shape == (): return eq_x & eq_y & eq_z & eq_weights & eq_comment \ & eq_sh_order & eq_system return (eq_x & eq_y & eq_z).all() & eq_weights & eq_comment \ & eq_sh_order & eq_system def _check_empty(self): """check if object is empty""" if self.cshape == (0,): raise ValueError('Object is empty.')
[docs] def cart2sph(x, y, z): """ Transforms from Cartesian to spherical coordinates. Spherical coordinates follow the common convention in Physics/Mathematics. The `colatitude` is measured downwards from the z-axis and is 0 at the North Pole and pi at the South Pole. The `azimuth` is 0 at positive x-direction and pi/2 at positive y-direction (counter clockwise rotation). Cartesian coordinates follow the right hand rule. .. math:: azimuth &= \\arctan(\\frac{y}{x}), colatitude &= \\arccos(\\frac{z}{r}), radius &= \\sqrt{x^2 + y^2 + z^2} .. math:: 0 < azimuth < 2 \\pi, 0 < colatitude < \\pi Parameters ---------- x : numpy array, number X values y : numpy array, number Y values z : numpy array, number Z values Returns ------- azimuth : numpy array, number Azimuth values colatitude : numpy array, number Colatitude values radius : numpy array, number Radii Notes ----- To ensure proper handling of the azimuth angle, the ``arctan2`` implementation from numpy is used. """ radius = np.sqrt(x**2 + y**2 + z**2) z_div_r = np.divide( z, radius, out=np.zeros_like(radius, dtype=float), where=radius != 0) colatitude = np.arccos(z_div_r) azimuth = np.mod(np.arctan2(y, x), 2 * np.pi) return azimuth, colatitude, radius
[docs] def sph2cart(azimuth, colatitude, radius): """ Transforms from spherical to Cartesian coordinates. Spherical coordinates follow the common convention in Physics/Mathematics. The `colatitude` is measured downwards from the z-axis and is 0 at the North Pole and pi at the South Pole. The `azimuth` is 0 at positive x-direction and pi/2 at positive y-direction (counter clockwise rotation). Cartesian coordinates follow the right hand rule. .. math:: x &= radius \\cdot \\sin(colatitude) \\cdot \\cos(azimuth), y &= radius \\cdot \\sin(colatitude) \\cdot \\sin(azimuth), z &= radius \\cdot \\cos(colatitude) .. math:: 0 < azimuth < 2 \\pi 0 < colatitude < \\pi Parameters ---------- azimuth : numpy array, number Azimuth values colatitude : numpy array, number Colatitude values radius : numpy array, number Radii Returns ------- x : numpy array, number X values y : numpy array, number Y values z : numpy array, number Z vales """ azimuth = np.atleast_1d(azimuth) colatitude = np.atleast_1d(colatitude) radius = np.atleast_1d(radius) r_sin_cola = radius * np.sin(colatitude) x = r_sin_cola * np.cos(azimuth) y = r_sin_cola * np.sin(azimuth) z = radius * np.cos(colatitude) x[np.abs(x) < np.finfo(x.dtype).eps] = 0 y[np.abs(y) < np.finfo(y.dtype).eps] = 0 z[np.abs(z) < np.finfo(x.dtype).eps] = 0 return x, y, z
[docs] def cart2cyl(x, y, z): """ Transforms from Cartesian to cylindrical coordinates. Cylindrical coordinates follow the convention that the `azimuth` is 0 at positive x-direction and pi/2 at positive y-direction (counter clockwise rotation). The `height` is identical to the z-coordinate and the `radius` is measured orthogonal from the z-axis. Cartesian coordinates follow the right hand rule. .. math:: azimuth &= \\arctan(\\frac{y}{x}), height &= z, radius &= \\sqrt{x^2 + y^2}, .. math:: 0 < azimuth < 2 \\pi Parameters ---------- x : numpy array, number X values y : numpy array, number Y values z : numpy array, number Z values Returns ------- azimuth : numpy array, number Azimuth values height : numpy array, number Height values radius : numpy array, number Radii Notes ----- To ensure proper handling of the azimuth angle, the ``arctan2`` implementation from numpy is used. """ azimuth = np.mod(np.arctan2(y, x), 2 * np.pi) if isinstance(z, np.ndarray): height = z.copy() else: height = z radius = np.sqrt(x**2 + y**2) return azimuth, height, radius
[docs] def cyl2cart(azimuth, height, radius): """ Transforms from cylindrical to Cartesian coordinates. Cylindrical coordinates follow the convention that the `azimuth` is 0 at positive x-direction and pi/2 at positive y-direction (counter clockwise rotation). The `height` is identical to the z-coordinate and the `radius` is measured orthogonal from the z-axis. Cartesian coordinates follow the right hand rule. .. math:: x &= radius \\cdot \\cos(azimuth), y &= radius \\cdot \\sin(azimuth), z &= height .. math:: 0 < azimuth < 2 \\pi Parameters ---------- azimuth : numpy array, number Azimuth values height : numpy array, number Height values radius : numpy array, number Radii Returns ------- x : numpy array, number X values y : numpy array, number Y values z : numpy array, number Z values Notes ----- To ensure proper handling of the azimuth angle, the ``arctan2`` implementation from numpy is used. """ azimuth = np.atleast_1d(azimuth) height = np.atleast_1d(height) radius = np.atleast_1d(radius) x = radius * np.cos(azimuth) y = radius * np.sin(azimuth) if isinstance(height, np.ndarray): z = height.copy() else: z = height x[np.abs(x) < np.finfo(x.dtype).eps] = 0 y[np.abs(y) < np.finfo(y.dtype).eps] = 0 z[np.abs(z) < np.finfo(x.dtype).eps] = 0 return x, y, z
[docs] def rad2deg(coordinates, domain='spherical'): """ Convert a copy of coordinates in radians to degree Parameters ---------- coordinates : array like N-dimensional array of shape `(..., 3)`. domain : str, optional Specifies what data are contained in `coordinates` ``'spherical'`` Spherical coordinates with angles contained in ``coordinates[..., 0:2]`` and radii in ``coordinates[..., 2]``. The radii are ignored during the conversion. ``'cylindrical'`` Cylindrical coordinates with angles contained in ``coordinates[..., 0]``, heights contained in ``coordinates[..., 1]``, and radii in ``coordinates[..., 2]``. The heights and radii are ignored during the conversion. Returns ------- coordinates : numpy array The converted coordinates of the same shape as the input data. """ return _convert_angles(coordinates, domain, 180/np.pi)
[docs] def deg2rad(coordinates, domain='spherical'): """ Convert a copy of coordinates in degree to radians Parameters ---------- coordinates : array like N-dimensional array of shape `(..., 3)`. domain : str, optional Specifies what data are contained in `coordinates` ``'spherical'`` Spherical coordinates with angles contained in ``coordinates[..., 0:2]`` and radii in ``coordinates[..., 2]``. The radii are ignored during the conversion. ``'cylindrical'`` Cylindrical coordinates with angles contained in ``coordinates[..., 0]``, heights contained in ``coordinates[..., 1]``, and radii in ``coordinates[..., 2]``. The heights and radii are ignored during the conversion. Returns ------- coordinates : numpy array The converted coordinates of the same shape as the input data. """ return _convert_angles(coordinates, domain, np.pi/180)
def _convert_angles(coordinates, domain, factor): """Private function called by rad2deg and deg2rad""" # check coordinates coordinates = np.atleast_2d(coordinates).astype(float) if coordinates.shape[-1] != 3: raise ValueError(('coordinates must be of shape (..., 3) but are of ' f'shape {coordinates.shape}')) # check domain and create mask if domain == 'spherical': mask = [True, True, False] elif domain == 'cylindrical': mask = [True, False, False] else: raise ValueError(("domain must be 'spherical' or 'cylindrical' but " f"is {domain}")) # convert data converted = coordinates.copy() converted[..., mask] = converted[..., mask] * factor return converted def _check_array_limits(values, lower_limit, upper_limit, variable_name=None): """ Values will be clipped to its range if deviations are below 2 eps for 32 bit float numbers otherwise Error is raised. Notes ----- This is mostly used for the colatitude angle. Parameters ---------- values : np.ndarray Input array angle lower_limit : float Lower limit for angle definition upper_limit : float Upper limit for angle definition variable_name : string Name of variable, just relevant for error message. 'value' by default. Returns ------- values : np.ndarray Clipped input values """ if variable_name is None: variable_name = 'value' if any(values < lower_limit): mask = values < lower_limit eps = np.finfo(float).eps if any(values[mask]+2*eps < lower_limit): raise ValueError( f'one or more {variable_name} are below ' f'{lower_limit} including 2 eps') values[mask] = lower_limit if any(values > upper_limit): mask = values > upper_limit eps = np.finfo(float).eps if any(values[mask] + 2*eps > upper_limit): raise ValueError( f'one or more {variable_name} are above ' f'{upper_limit} including 2 eps') values[mask] = upper_limit return values