r"""
The following introduces the
:py:func:`Coordinates class <pyfar.Coordinates>`
and the coordinate systems that are available in pyfar. Available sampling
schemes are listed at :py:mod:`spharpy.samplings <spharpy.samplings>`.
:ref:`Examples <gallery:/gallery/interactive/pyfar_coordinates.ipynb>` for
working with Coordinates objects are part of the pyfar gallery.
Different coordinate systems are frequently used in acoustics research and
handling sampling points and different systems can be cumbersome. The
Coordinates class was designed with this in mind. It stores coordinates in
cartesian coordinates internally and can convert to all coordinate systems
listed below. Additionally, the class can query and plot coordinates
points. Addition and subtraction are supported with numbers and Coordinates
objects, while multiplication and division are supported with numbers only.
All arithmetic operations are performed element-wise on Cartesian coordinates
using the appropriate operator.
Functions for converting coordinates not stored in a Coordinates object
are available for convenience. However, it is strongly recommended to use the
Coordinates class for all conversions.
.. _coordinate_systems:
Coordinate Systems
------------------
Each coordinate system has a unique name, e.g., `spherical_elevation`, and is
defined by three coordinates, in this case `azimuth`, `elevation`, and
`radius`. The available coordinate systems are shown in the image below.
|coordinate_systems|
.. _coordinates:
Coordinates
-----------
The unit for length for the coordinates is always meter, while the unit for
angles is radians. Each coordinate is unique, but can appear in multiple
coordinate systems, e.g., the `azimuth` angle is contained in two coordinate
systems (`spherical_colatitude` and `spherical_elevation`). The table below
lists all coordinates.
.. list-table::
:widths: 25 75
:header-rows: 1
* - Coordinate
- Descriptions
* - :py:func:`~pyfar.Coordinates.x`,
:py:func:`~pyfar.Coordinates.y`,
:py:func:`~pyfar.Coordinates.z`
- x, y, z coordinate of a right handed Cartesian coordinate system in
meter (:math:`-\infty` < x,y,z < :math:`\infty`).
* - :py:func:`~pyfar.Coordinates.azimuth`
- Counter clock-wise angle in the x-y plane of the right handed Cartesian
coordinate system in radians. :math:`0` radians are defined in positive
x-direction, :math:`\pi/2` radians in positive y-direction and so on
(:math:`-\infty` < azimuth < :math:`\infty`, :math:`2\pi`-cyclic).
* - :py:func:`~pyfar.Coordinates.colatitude`
- Angle in the x-z plane of the right handed Cartesian coordinate system
in radians. :math:`0` radians colatitude are defined in positive
z-direction, :math:`\pi/2` radians in positive x-direction, and
:math:`\pi` in negative z-direction
(:math:`0\leq` colatitude :math:`\leq\pi`). The colatitude is a
variation of the elevation angle.
* - :py:func:`~pyfar.Coordinates.elevation`
- Angle in the x-z plane of the right handed Cartesian coordinate system
in radians. :math:`0` radians elevation are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction, and
:math:`-\pi/2` in negative z-direction
(:math:`-\pi/2\leq` elevation :math:`\leq\pi/2`). The elevation is a
variation of the colatitude.
* - :py:func:`~pyfar.Coordinates.lateral`
- Counter clock-wise angle in the x-y plane of the right handed Cartesian
coordinate system in radians. :math:`0` radians are defined in positive
x-direction, :math:`\pi/2` radians in positive y-direction and
:math:`-\pi/2` in negative y-direction
(:math:`-\pi/2\leq` lateral :math:`\leq\pi/2`).
* - :py:func:`~pyfar.Coordinates.polar`
- Angle in the x-z plane of the right handed Cartesian coordinate system
in radians. :math:`0` radians polar angle are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction,
:math:`\pi` in negative x-direction and so on
(:math:`-\infty` < polar < :math:`\infty`, :math:`2\pi`-cyclic).
* - :py:func:`~pyfar.Coordinates.frontal`
- Angle in the y-z plane of the right handed Cartesian coordinate system
in radians. :math:`0` radians frontal angle are defined in positive
y-direction, :math:`\pi/2` radians in positive z-direction,
:math:`\pi` in negative y-direction and so on
(:math:`-\infty` < frontal < :math:`\infty`, :math:`2\pi`-cyclic).
* - :py:func:`~pyfar.Coordinates.upper`
- Angle in the x-z plane of the right handed Cartesian coordinate system
in radians. :math:`0` radians upper angle are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction, and
:math:`\pi` in negative x-direction
(:math:`0\leq` upper :math:`\leq\pi`).
* - :py:func:`~pyfar.Coordinates.radius`
- Distance to the origin of the right handed Cartesian coordinate system
in meters (:math:`0` < radius < :math:`\infty`).
* - :py:func:`~pyfar.Coordinates.rho`
- Radial distance to the the z-axis of the right handed Cartesian
coordinate system (:math:`0` < rho < :math:`\infty`).
.. |coordinate_systems| image:: resources/coordinate_systems.png
:width: 100%
:alt: pyfar coordinate systems
"""
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 `name`
(`domain`, `convention`, and `unit`. The `unit` will be deprecated in pyfar
v0.8.0 in favor of fixed default units, see :ref:`coordinate_systems` and
:ref:`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, stacklevel=2)
# 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
[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
see :ref:`coordinate_systems` and :ref:`coordinates` for
more information.
Parameters
----------
x : ndarray, number
X coordinate of a right handed Cartesian coordinate system in
meters (:math:`-\infty` < x < :math:`\infty`).
y : ndarray, number
Y coordinate of a right handed Cartesian coordinate system in
meters ():math:`-\infty` < y < :math:`\infty`).
z : ndarray, number
Z coordinate of a right handed Cartesian coordinate system in
meters (:math:`-\infty` < z < :math:`\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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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, stacklevel=2)
# 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."))
if unit != 'met':
# Can not be tested. Will only be raised if a coordinate system
# is not fully implemented.
raise ValueError(
f"Unit for {unit} 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): # noqa: ARG002
"""
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, stacklevel=2)
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'):
r"""
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
----------
angles_1: array like, number
Points for the first coordinate, see table above.
angles_2: array like, number
Points for the second coordinate, see table above.
radius: array like, number
Distance to the origin of the right handed Cartesian
coordinate system in meters
(:math:`0` < radius < :math:`\infty`).
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, stacklevel=2)
# 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, stacklevel=2)
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): # noqa: ARG002
"""
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, stacklevel=2)
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): # noqa: ARG002
# 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, stacklevel=2)
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 the points
return angles_1, angles_2, radius
[docs]
def set_cyl(self, azimuth, z, radius_z, convention='top', unit='rad'):
r"""
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
----------
azimuth : array like, number
Counter clock-wise angle in the x-y plane of the right handed
Cartesian coordinate system in radians. :math:`0` radians are
defined in positive x-direction, :math:`\pi/2` radians in
positive y-direction and so on
(:math:`-\infty` < azimuth < :math:`\infty`, :math:`2\pi`-cyclic).
z : array like, number
Z coordinate of a right handed Cartesian coordinate system
in meters (:math:`-\infty` < z < :math:`\infty`).
radius_z : array like, number
radius in z plane in meters
(:math:`0` < radius_z < :math:`\infty`).
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, stacklevel=2)
self._set_cyl(azimuth, z, radius_z, convention, unit)
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, stacklevel=2)
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): # noqa: ARG002
"""
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, stacklevel=2)
if not convention == 'top':
raise ValueError(
f"Conversion for {convention} is not implemented.")
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, stacklevel=2)
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, stacklevel=2)
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.
"""
if value is not None:
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.SamplingSphere."),
PyfarDeprecationWarning, stacklevel=2)
self._sh_order = int(value) if value is not None else None
@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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
see :ref:`coordinate_systems` and :ref:`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
(:math:`-\infty` < x < :math:`\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
(:math:`-\infty` < y < :math:`\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
(:math:`-\infty` < z < :math:`\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 (:math:`0` < rho < :math:`\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"""
Distance to the origin of the right handed Cartesian coordinate system
in meters (:math:`0` < radius < :math:`\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. :math:`0` radians are defined in positive
x-direction, :math:`\pi/2` radians in positive y-direction and so on
(:math:`-\infty` < azimuth < :math:`\infty`, :math:`2\pi`-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. :math:`0` radians elevation are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction, and
:math:`-\pi/2` in negative z-direction
(:math:`-\pi/2\leq` elevation :math:`\leq\pi/2`). 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. :math:`0` radians colatitude are defined in positive
z-direction, :math:`\pi/2` radians in positive x-direction, and
:math:`\pi` in negative z-direction
(:math:`0\leq` colatitude :math:`\leq\pi`). 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. :math:`0` radians frontal angle are defined in positive
y-direction, :math:`\pi/2` radians in positive z-direction,
:math:`\pi` in negative y-direction and so on
(:math:`-\infty` < frontal < :math:`\infty`, :math:`2\pi`-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. :math:`0` radians upper angle are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction, and
:math:`\pi` in negative x-direction
(:math:`0\leq` upper :math:`\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. :math:`0` radians are defined in positive
x-direction, :math:`\pi/2` radians in positive y-direction and
:math:`-\pi/2` in negative y-direction
(:math:`-\pi/2\leq` lateral :math:`\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. :math:`0` radians polar angle are defined in positive
x-direction, :math:`\pi/2` radians in positive z-direction,
:math:`\pi` in negative x-direction and so on
(:math:`-\infty` < polar < :math:`\infty`, :math:`2\pi`-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, stacklevel=2)
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
:py:func:`matplotlib.pyplot.scatter`.
If a mask is provided and the key `c` is contained in kwargs, it
will be overwritten.
Returns
-------
ax : :py:class:`~mpl_toolkits.mplot3d.axes3d.Axes3D`
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',
radius_tol=None):
"""
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.
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.
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 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 :py:class:`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 radius_tol is None:
radius_tol = 2 * np.finfo(self.x.dtype).resolution
if not isinstance(radius_tol, float) or radius_tol < 0:
raise ValueError("radius_tol must be a non negative number.")
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 > radius_tol:
raise ValueError(
f"find_nearest_sph only works if all points have the same \
radius. Differences are larger than {radius_tol}")
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] = (index_multi[kk], )
else:
index = (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 :py:class:`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 :py:class:`numpy.finfo`.
Returns
-------
index : tuple of array
Indices of the containing coordinates. Arrays of shape
(find.cshape).
Notes
-----
This is a wrapper for :py:class:`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_radians':
# 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 euclidean distance
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] = (index[i], )
else:
index = (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_1 : array like, number
First coordinate of the points to which the
nearest neighbors are searched.
points_2 : array like, number
Second coordinate of the points to which the
nearest neighbors are searched.
points_3 : array like, number
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
-----
:py:class:`scipy.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, stacklevel=2)
# 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_1 : array like, number
First coordinate of the points to which the
nearest neighbors are searched.
points_2 : array like, number
Second coordinate of the points to which the
nearest neighbors are searched.
points_3 : array like, number
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
-----
:py:class:`scipy.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, stacklevel=2)
# 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_1 : array like, number
First coordinate of the points to which the
nearest neighbors are searched.
points_2 : array like, number
Second coordinate of the points to which the
nearest neighbors are searched.
points_3 : array like, number
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
-----
:py:class:`scipy.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, stacklevel=2)
# 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, stacklevel=2)
# 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 :py:class:`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'
unit: 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
----------
x : array like, number
First coordinate of the points in cartesian.
y : array like, number
Second coordinate of the points in cartesian.
z : array like, number
Third coordinate of the points in cartesian.
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))
# determine shape
shapes = np.broadcast_shapes(x.shape, y.shape, z.shape)
# broadcast to same shape
x = np.broadcast_to(x, shapes)
y = np.broadcast_to(y, shapes)
z = np.broadcast_to(z, shapes)
# set writeable, to make sure that the class does not become read-only
x.setflags(write=True)
y.setflags(write=True)
z.setflags(write=True)
# 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, copy=True, dtype=None):
"""Instances of Coordinates behave like `numpy.ndarray`, array_like."""
# copy to avoid changing the coordinate system of the original object
return np.array(self.cartesian, copy=copy, dtype=dtype)
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."""
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 __add__(self, other):
"""Add two numbers/Coordinates objects."""
return _arithmetics(self, other, 'add')
def __radd__(self, other):
"""Add two numbers/Coordinates objects."""
return _arithmetics(other, self, 'add')
def __sub__(self, other):
"""Subtract two numbers/Coordinates objects."""
return _arithmetics(self, other, 'sub')
def __rsub__(self, other):
"""Subtract two numbers/Coordinates objects."""
return _arithmetics(other, self, 'sub')
def __mul__(self, other):
"""Multiply Coordinates object with number."""
return _arithmetics(self, other, 'mul')
def __rmul__(self, other):
"""Multiply number with Coordinates object."""
return _arithmetics(other, self, 'mul')
def __div__(self, other):
"""Divide Coordinates object with number."""
return _arithmetics(self, other, 'div')
def __truediv__(self, other):
"""Divide Coordinates object with number."""
return _arithmetics(self, other, 'div')
def __rtruediv__(self, other):
"""Divide number with Coordinates object."""
return _arithmetics(other, self, 'div')
def __rdiv__(self, other):
"""Divide number with Coordinates object."""
return _arithmetics(other, self, 'div')
def _check_empty(self):
"""Check if object is empty."""
if self.cshape == (0,):
raise ValueError('Object is empty.')
[docs]
def dot(a, b):
r"""Dot product of two Coordinates objects.
.. math::
\vec{a} \cdot \vec{b}
= a_x \cdot b_x + a_y \cdot b_y + a_z \cdot b_z
Parameters
----------
a : pf.Coordinates
first argument, must be broadcastable with b
b : pf.Coordinates
second argument, much be broadcastable with a
Returns
-------
result : np.ndarray
array with the dot product of the two objects
Examples
--------
>>> import pyfar as pf
>>> a = pf.Coordinates(1, 0, 0)
>>> b = pf.Coordinates(1, 0, 0)
>>> pf.dot(a, b)
array([1.])
"""
if not isinstance(a, Coordinates) or not isinstance(b, Coordinates):
raise TypeError(
"Dot product is only possible with Coordinates objects.")
return a.x * b.x + a.y * b.y + a.z * b.z
[docs]
def cross(a, b):
r"""Cross product of two Coordinates objects.
.. math::
\vec{a} \times \vec{b}
= (a_y \cdot b_z - a_z \cdot b_y) \cdot \hat{x}
+ (a_z \cdot b_x - a_x \cdot b_z) \cdot \hat{y}
+ (a_x \cdot b_y - a_y \cdot b_x) \cdot \hat{z}
Parameters
----------
a : pf.Coordinates
first argument, must be broadcastable with b
b : pf.Coordinates
second argument, much be broadcastable with a
Returns
-------
result : pf.Coordinates
new Coordinates object with the cross product of the two objects
Examples
--------
>>> import pyfar as pf
>>> a = pf.Coordinates(1, 0, 0)
>>> b = pf.Coordinates(0, 1, 0)
>>> result = pf.cross(a, b)
>>> result.cartesian
array([0., 0., 1.])
"""
if not isinstance(a, Coordinates) or not isinstance(b, Coordinates):
raise TypeError(
"Dot product is only possible with Coordinates objects.")
new = Coordinates()
new.cartesian = np.zeros(np.broadcast_shapes(
a.cartesian.shape, b.cartesian.shape))
# apply cross product
new.x = a.y * b.z - a.z * b.y
new.y = a.z * b.x - a.x * b.z
new.z = a.x * b.y - a.y * b.x
return new
def _arithmetics(first, second, operation):
"""Add or Subtract two Coordinates objects, numbers or arrays.
Parameters
----------
first : Coordinates, number, array
first operand
second : Coordinates, number, array
second operand
operation : 'add', 'sub', 'mul', 'div'
whether to add or subtract the two objects
Returns
-------
new : Coordinates
result of the operation
"""
# convert data
data = []
num_objects = 0
for obj in [first, second]:
if isinstance(obj, Coordinates):
data.append(obj.cartesian)
num_objects += 1
elif isinstance(obj, (int, float)):
data.append(np.array(obj))
else:
if operation == 'add':
op = 'Addition'
elif operation == 'sub':
op = 'Subtraction'
elif operation == 'mul':
op = 'Multiplication'
elif operation == 'div':
op = 'Division'
raise TypeError(
f"{op} is only possible with Coordinates or number.")
if operation in ['mul', 'div'] and num_objects > 1:
raise TypeError(
"Multiplication and division are only possible with one "
"Coordinates object.")
# broadcast shapes
shape = np.broadcast_shapes(data[0].shape, data[1].shape)
new = pf.Coordinates()
new.cartesian = np.zeros(shape)
# perform operation
if operation == 'add':
new.cartesian = data[0] + data[1]
elif operation == 'sub':
new.cartesian = data[0] - data[1]
elif operation == 'mul':
new.cartesian = data[0] * data[1]
elif operation == 'div':
new.cartesian = data[0] / data[1]
return new
[docs]
def cart2sph(x, y, z):
r"""
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
:py:data:`numpy.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):
r"""
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):
r"""
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
:py:data:`numpy.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):
r"""
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
:py:data:`numpy.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