# -*- mode: python; coding: utf-8 -*
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Methods for doing spherical coordinate transformations on vectors."""
import numpy as np
[docs]def r_hat(theta, phi):
"""
Get the r hat unit vectors in cartesian coordinates for points on a sphere.
Parameters
----------
theta, phi : float
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
Returns
-------
array of float
Array of r hat vectors, shape (3, Npoints)
"""
theta = np.array(theta)
phi = np.array(phi)
if theta.shape != phi.shape:
raise ValueError("theta and phi must have the same shape")
rhx = np.cos(phi) * np.sin(theta)
rhy = np.sin(phi) * np.sin(theta)
rhz = np.cos(theta)
return np.stack((rhx, rhy, rhz))
[docs]def theta_hat(theta, phi):
"""
Get the theta hat unit vectors in cartesian coordinates for points on a sphere.
Parameters
----------
theta, phi : float
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
Returns
-------
array of float
Array of theta hat vectors, shape (3, Npoints)
"""
theta = np.array(theta)
phi = np.array(phi)
if theta.shape != phi.shape:
raise ValueError("theta and phi must have the same shape")
thx = np.cos(phi) * np.cos(theta)
thy = np.sin(phi) * np.cos(theta)
thz = -np.sin(theta)
return np.stack((thx, thy, thz))
[docs]def phi_hat(theta, phi):
"""
Get the phi hat unit vectors in cartesian coordinates for points on a sphere.
Parameters
----------
theta, phi : float
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
Returns
-------
array of float
Array of phi hat vectors, shape (3, Npoints)
"""
theta = np.array(theta)
phi = np.array(phi)
if theta.shape != phi.shape:
raise ValueError("theta and phi must have the same shape")
phx = -np.sin(phi)
phy = np.cos(phi)
phz = np.zeros_like(phi)
return np.stack((phx, phy, phz))
[docs]def rotate_points_3d(rot_matrix, theta, phi):
"""
Get the spherical coordinates of the point under a 3d rotation.
Finds the spherical coordinates for point p specified by p = R . q,
where q is the 3D position vector of the point specified by (theta,phi) and
R is the 3D rotation matrix that relates two coordinate charts.
The accuracy of this method may not be good enough near pols in either
coordinate system.
Parameters
----------
rot_matrix : array-like of float
rotation matrix to use
theta, phi : float
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
Returns
-------
beta, alpha : float
The theta, phi coordinates for the point on the sphere (using normal
mathematical conventions) in the rotated frame.
"""
# This is NOT written to be vectorized for multiple (theta, phi)
rot_matrix = np.array(rot_matrix)
if rot_matrix.shape != (3, 3):
raise ValueError("rot_matrix must be a 3x3 array")
# Replace with function call?
q_hat_1 = np.cos(phi) * np.sin(theta)
q_hat_2 = np.sin(phi) * np.sin(theta)
q_hat_3 = np.cos(theta)
q_hat = np.stack((q_hat_1, q_hat_2, q_hat_3))
# Should test for shape of p_hat
p_hat = np.einsum("ab...,b...->a...", rot_matrix, q_hat)
# Should write a function to do this as well, i.e., pull back angles from
# a vector
if np.isclose(p_hat[2], 1.0, rtol=0.0, atol=1e-12):
p_hat[2] = 1.0
beta = np.arccos(p_hat[2])
alpha = np.arctan2(p_hat[1], p_hat[0])
if alpha < 0:
alpha += 2.0 * np.pi
return (beta, alpha)
[docs]def spherical_basis_vector_rotation_matrix(
theta, phi, rot_matrix, beta=None, alpha=None
):
"""
Get the rotation matrix to take vectors in the theta/phi basis to a new reference frame.
Given a position (`theta`, `phi`) in “standard mathematical” coordinates
(0 < `theta` < pi, 0 < `phi` < 2 pi) which will typically be an ICRS RA/Dec
coordinate appropriately converted, and the point to which it is transformed
in another standard mathematical coordinate system (`beta`, `alpha`), which
will typically be local telescope Alt/Az appropriately converted, and a
3 x 3 rotation matrix `rot_matrix` which connects those two points,
calculate the rotation matrix which rotates the basis vectors associated
with (`theta`, `phi`) to those associated with (`beta`, `alpha`).
Parameters
----------
theta, phi : float
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
rot_matrix : array-like of float
Rotation matrix that takes 3-vectors from (theta, phi) to (beta, alpha)
beta, alpha : float, optional
The theta, phi coordinates for the point on the sphere (using normal
mathematical conventions) in the rotated frame. If either is not provided,
they are calculated using `rotate_points_3d`. Note these may
not be as exact as values calculated from astropy.
Returns
-------
array of float
2 x 2 rotation matrix that takes vectors in the theta/phi basis to
the beta/alpha basis.
"""
if alpha is None or beta is None:
beta, alpha = rotate_points_3d(rot_matrix, theta, phi)
th = theta_hat(theta, phi)
ph = phi_hat(theta, phi)
bh = np.einsum("ab...,b...->a...", rot_matrix.T, theta_hat(beta, alpha))
cosX = np.einsum("a...,a...", bh, th)
sinX = np.einsum("a...,a...", bh, ph)
return np.array([[cosX, sinX], [-sinX, cosX]])
[docs]def axis_angle_rotation_matrix(axis, angle):
"""
Get the rotation matrix using Rodrigues' rotation matrix formula.
Parameters
----------
axis : array-like of float
3 element unit vector specifying the axis to rotate around.
angle : float
angle to rotate by in radians
Returns
-------
array
3x3 rotation matrix to rotate vectors by `angle` around `axis`.
"""
if axis.shape != (3,):
raise ValueError("axis must be a must be length 3 vector")
if not is_unit_vector(axis):
raise ValueError("axis must be a unit vector")
K_matrix = np.array(
[[0.0, -axis[2], axis[1]], [axis[2], 0.0, -axis[0]], [-axis[1], axis[0], 0.0]]
)
I_matrix = np.identity(3)
rot_matrix = (
I_matrix
+ np.sin(angle) * K_matrix
+ (1.0 - np.cos(angle)) * np.dot(K_matrix, K_matrix)
)
return rot_matrix
[docs]def is_orthogonal(matrix, tol=1e-15):
"""
Test for matrix orthogonality.
Parameters
----------
matrix : array-like of float
square matrix to test
Returns
-------
bool
True if `matrix` is orthogonal, False otherwise.
"""
return np.allclose(np.matmul(matrix, matrix.T), np.eye(3), atol=tol)
[docs]def is_unit_vector(vec, tol=1e-15):
"""
Test for unit vectors.
Parameters
----------
vec : array-like of float
vector to test
Returns
-------
bool
True if `vec` is a unit vector, False otherwise.
"""
return np.allclose(np.dot(vec, vec), 1, atol=tol)
[docs]def vecs2rot(r1=None, r2=None, theta1=None, phi1=None, theta2=None, phi2=None):
"""
Get the rotation matrix that connects two points or unit vectors on the sphere.
Parameters
----------
r1, r2 : array-like of float, optional
length 3 unit vectors
theta1, phi1, theta2, phi2 : float, optional
The co-latitude and azimuth coordinates, respectively, for a point
on the sphere, in radians. Azimuth is defined with respect to the
x axis, co-latitude is the angle with the positive z axis.
Ignored if r1 and r2 are supplied.
Returns
-------
array
3x3 rotation matrix that rotates the first point or vector into the other.
"""
if r1 is None or r2 is None:
if theta1 is None or phi1 is None or theta2 is None or phi2 is None:
raise ValueError(
"Either r1 and r2 must be supplied or all of "
"theta1, phi1, theta2 and phi2 must be supplied."
)
r1 = r_hat(theta1, phi1)
r2 = r_hat(theta2, phi2)
assert is_unit_vector(r1), "r1 is not a unit vector: " + str(r1)
assert is_unit_vector(r2), "r2 is not a unit vector: " + str(r2)
else:
r1 = np.array(r1)
r2 = np.array(r2)
if r1.shape != (3,) or r2.shape != (3,):
raise ValueError("r1 and r2 must be length 3 vectors")
if not is_unit_vector(r1) or not is_unit_vector(r2):
raise ValueError("r1 and r2 must be unit vectors")
norm = np.cross(r1, r2)
# Note that Psi is between 0 and pi
sinPsi = np.sqrt(np.dot(norm, norm))
n_hat = norm / sinPsi # Trouble lurks if Psi = 0.
cosPsi = np.dot(r1, r2)
Psi = np.arctan2(sinPsi, cosPsi)
rotation = axis_angle_rotation_matrix(n_hat, Psi)
assert is_unit_vector(n_hat), "n_hat is not a unit vector: " + str(n_hat)
assert is_orthogonal(rotation), "rotation matrix is not orthogonal: " + str(
rotation
)
return rotation