Skip to content

Commit

Permalink
Merge pull request #16 from sot/pitch-yaw-utils
Browse files Browse the repository at this point in the history
Add utility functions, add units tests and package properly
  • Loading branch information
taldcroft authored Aug 27, 2021
2 parents 235255e + 692ee44 commit 2b70301
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 44 deletions.
147 changes: 123 additions & 24 deletions Ska/Sun.py → Ska/Sun/Sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
Utility for calculating sun position and pitch angle.
"""

import Quaternion
from Quaternion import Quat
from chandra_aca.transform import radec_to_eci
from Chandra.Time import DateTime
from math import cos, sin, acos, atan2, asin, pi, radians, degrees, ceil
from math import cos, sin, acos, atan2, asin, pi, radians, degrees
import numpy as np
import Ska.quatutil
import ska_helpers

__version__ = ska_helpers.get_version(__name__)


# The position() method is a modification of
Expand Down Expand Up @@ -50,15 +48,15 @@ def position(time):
Code modified from http://idlastro.gsfc.nasa.gov/ftp/pro/astro/sunpos.pro
Example::
>>> import Ska.Sun
>>> Ska.Sun.position('2008:002:00:01:02')
(281.90344855695275, -22.9892737322084)
:param time: Input time (Chandra.Time compatible format)
:rtype: RA, Dec in decimal degrees (J2000).
"""

t = (DateTime(time).jd - 2415020)/(36525.0)

dtor = pi/180
Expand All @@ -70,7 +68,7 @@ def position(time):
me = 358.475844 + (35999.049750*t) % 360.0
ellcor = (6910.1 - (17.2*t))*sin(me*dtor) + 72.3*sin(2.0*me*dtor)
l = l + ellcor

# allow for the Venus perturbations using the mean anomaly of Venus MV
mv = 212.603219 + (58517.803875*t) % 360.0
vencorr = 4.8 * cos((299.1017 + mv - me)*dtor) + \
Expand All @@ -79,7 +77,7 @@ def position(time):
1.6 * cos((345.2533 + 3.0 * mv - 4.0 * me )*dtor) + \
1.0 * cos((318.15 + 3.0 * mv - 5.0 * me )*dtor)
l = l + vencorr

# Allow for the Mars perturbations using the mean anomaly of Mars MM
mm = 319.529425 + ( 19139.858500 * t) % 360.0
marscorr = 2.0 * cos((343.8883 - 2.0 * mm + 2.0 * me)*dtor ) + \
Expand All @@ -96,21 +94,21 @@ def position(time):

# Allow for the Moons perturbations using the mean elongation of the Moon
# from the Sun D
d = 350.7376814 + ( 445267.11422 * t) % 360.0
d = 350.7376814 + ( 445267.11422 * t) % 360.0
mooncorr = 6.5 * sin(d*dtor);
l = l + mooncorr;

# Allow for long period terms
longterm = 6.4 * sin(( 231.19 + 20.20 * t )*dtor)
l = l + longterm
l = ( l + 2592000.0) % 1296000.0
l = ( l + 2592000.0) % 1296000.0
longmed = l/3600.0

# Allow for Aberration
l = l - 20.5;

# Allow for Nutation using the longitude of the Moons mean node OMEGA
omega = 259.183275 - ( 1934.142008 * t ) % 360.0
omega = 259.183275 - ( 1934.142008 * t ) % 360.0
l = l - 17.2 * sin(omega*dtor)

# Form the True Obliquity
Expand All @@ -119,21 +117,22 @@ def position(time):
# Form Right Ascension and Declination
l = l/3600.0;
ra = atan2( sin(l*dtor) * cos(oblt*dtor) , cos(l*dtor) );

while ((ra < 0) or ( ra > (2*pi))):
if (ra < 0):
ra += (2*pi)
if (ra > (2*pi)):
ra -= (2*pi)

dec = asin(sin(l*dtor) * sin(oblt*dtor));
return ra/dtor, dec/dtor

return ra/dtor, dec/dtor

def sph_dist(a1, d1, a2, d2):
"""Calculate spherical distance between two sky positions. Not highly
accurate for very small angles. This function is deprecated, use
Ska.astro.sph_dist() instead.
"""Calculate spherical distance between two sky positions.
Not highly accurate for very small angles. This function is deprecated, use
agasc.sphere_dist() instead.
:param a1: RA position 1 (deg)
:param d1: dec position 1 (deg)
Expand Down Expand Up @@ -181,7 +180,7 @@ def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None):
``sun_ra`` and ``sun_dec`` instead of ``time``.
Example::
>>> Ska.Sun.nominal_roll(205.3105, -14.6925, time='2011:019:20:51:13')
68.830209134280665 # vs. 68.80 for obsid 12393 in JAN1711A
Expand All @@ -204,7 +203,7 @@ def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None):
body_y = body_y / np.sqrt(np.sum(body_y**2))
body_z = np.cross(body_x, body_y)
body_z = body_z / np.sqrt(np.sum(body_z**2)) # shouldn't be needed but do it anyway
q_att = Quaternion.Quat(np.array([body_x, body_y, body_z]).transpose())
q_att = Quat(np.array([body_x, body_y, body_z]).transpose())
return q_att.roll


Expand All @@ -224,7 +223,6 @@ def off_nominal_roll(att, time):
:returns: off nominal roll angle (deg)
"""
from Quaternion import Quat

q = Quat(att)
roll = q.roll
Expand All @@ -238,3 +236,104 @@ def off_nominal_roll(att, time):
off_nom_roll -= 360

return off_nom_roll


def get_sun_pitch_yaw(ra, dec, time=None, sun_ra=None, sun_dec=None):
"""Get Sun pitch and yaw angles of Sky coordinate(s).
:param ra: float, ndarray
RA(s)
:param dec: float, ndarray
Dec(s)
:param time: date-like, optional
Date of observation. If not given, use ``sun_ra`` and ``sun_dec``
if provided or else use current date.
:param sun_ra: float, optional
RA of sun. If not given, use estimated sun RA at ``date``.
:param sun_dec: float, optional
Dec of sun. If not given, use estimated sun dec at ``date``.
:returns:
2-tuple (pitch, yaw) in degrees.
"""
# If not provided calculate sun RA and Dec using a low-accuracy ephemeris
if sun_ra is None or sun_dec is None:
sun_ra, sun_dec = position(time)

# Compute attitude vector in ECI
att_eci = radec_to_eci(ra, dec)

# Make a Sun frame defined by vector to the Sun assuming roll=0
sun_frame = Quat([sun_ra, sun_dec, 0])
# Sun frame inverse rotation matrix.
sun_frame_rot = sun_frame.transform.T

# Compute attitude vector in Sun frame.
att_sun = np.einsum('...jk,...k->...j', sun_frame_rot, att_eci)

# Usual for pitch and yaw. The yaw is set to match ORviewer:
# get_sun_pitch_yaw(109, 55.3, time='2021:242') ~ (60, 30)
# get_sun_pitch_yaw(238.2, -58.9, time='2021:242') ~ (90, 210)
pitch = np.arccos(att_sun[..., 0])
yaw = -np.arctan2(att_sun[..., 1], att_sun[..., 2]) # -pi <= yaw < pi
yaw = yaw % (2 * np.pi) # 0 <= yaw < 2pi

return np.rad2deg(pitch), np.rad2deg(yaw)


def apply_sun_pitch_yaw(att, pitch=0, yaw=0,
time=None, sun_ra=None, sun_dec=None):
"""Apply pitch(es) and yaw(s) about Sun line to an attitude.
:param att: Quaternion-like
Attitude(s) to be rotated.
:param pitch: float, ndarray
Sun pitch offsets (deg)
:param yaw: float, ndarray
Sun yaw offsets (deg)
:param sun_ra: float, optional
RA of sun. If not given, use estimated sun RA at ``time``.
:param sun_dec: float, optional
Dec of sun. If not given, use estimated sun dec at ``time``.
:returns: Quat
Modified attitude(s)
"""
if not isinstance(att, Quat):
att = Quat(att)

# If not provided calculate sun RA and Dec using a low-accuracy ephemeris
if sun_ra is None or sun_dec is None:
sun_ra, sun_dec = position(time)

# Compute Sun and attitude vectors in ECI
eci_sun = radec_to_eci(sun_ra, sun_dec)
eci_att = radec_to_eci(att.ra, att.dec)

# Rotation vector for apply pitch about Sun line
pitch_rot_vec = np.cross(eci_sun, eci_att)
pitch_rot_vec = pitch_rot_vec / np.linalg.norm(pitch_rot_vec)

# Broadcast input pitch and yaw to a common shape
pitches, yaws = np.broadcast_arrays(pitch, yaw)
out_shape = pitches.shape
# Get pitches and yaws as 1-d iterables
pitches = np.atleast_1d(pitches).ravel()
yaws = np.atleast_1d(yaws).ravel()

qs = [] # Output quaternions as a list of 4-element ndarrays
for pitch, yaw in zip(pitches, yaws):
att_out = att
if pitch != 0:
# Pitch rotation is in the plane defined by attitude vector and the
# body-to-sun vector.
att_out = att_out.rotate_about_vec(pitch_rot_vec, pitch)
if yaw != 0:
# Yaw rotation is about the body-to-sun vector.
att_out = att_out.rotate_about_vec(eci_sun, yaw)
qs.append(att_out.q)

# Reshape into correct output shape and return corresponding quaternion
qs = np.array(qs).reshape(out_shape + (4,))
return Quat(q=qs)
14 changes: 14 additions & 0 deletions Ska/Sun/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

import ska_helpers
from .Sun import * # noqa

__version__ = ska_helpers.get_version('Ska.Sun')


def test(*args, **kwargs):
'''
Run py.test unit tests.
'''
import testr
return testr.test(*args, **kwargs)
76 changes: 76 additions & 0 deletions Ska/Sun/tests/test_sun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

import numpy as np
from Quaternion import Quat
from ..Sun import (apply_sun_pitch_yaw, get_sun_pitch_yaw, nominal_roll,
off_nominal_roll, position, pitch as sun_pitch)


def test_position():
ra, dec = position('2008:002:00:01:02')
assert np.allclose((ra, dec), (281.903448, -22.989273))


def test_nominal_roll():
roll = nominal_roll(205.3105, -14.6925, time='2011:019:20:51:13')
assert np.allclose(roll, 68.83020) # vs. 68.80 for obsid 12393 in JAN1711A


def test_off_nominal_roll_and_pitch():
att = (198.392135, 36.594359, 33.983322) # RA, Dec, Roll of obsid 16354
oroll = off_nominal_roll(att, '2015:335:00:00:00') # NOT the 16354 time
assert np.isclose(oroll, -12.224010)

date = '2015:077:01:07:04'
pitch = sun_pitch(att[0], att[1], time=date)
assert np.isclose(pitch, 139.5651) # vs. 139.59 in SOT MP page

pitch, _ = get_sun_pitch_yaw(att[0], att[1], time=date)
assert np.isclose(pitch, 139.5651) # vs. 139.59 in SOT MP page


def test_apply_get_sun_pitch_yaw():
"""Test apply and get sun_pitch_yaw with multiple components"""
att = apply_sun_pitch_yaw([0, 45, 0], pitch=[0, 10, 20], yaw=[0, 5, 10],
sun_ra=0, sun_dec=90)
pitch, yaw = get_sun_pitch_yaw(att.ra, att.dec, sun_ra=0, sun_dec=90)
assert np.allclose(pitch, 45 + np.array([0, 10, 20]))
assert np.allclose(yaw, 180 + np.array([0, 5, 10]))


def test_apply_sun_pitch_yaw():
"""Basic test of apply_sun_pitch_yaw"""
att = Quat(equatorial=[0, 45, 0])
att2 = apply_sun_pitch_yaw(att, pitch=10, yaw=0, sun_ra=0, sun_dec=0)
assert np.allclose((att2.ra, att2.dec), (0, 55))

att2 = apply_sun_pitch_yaw(att, pitch=0, yaw=10, sun_ra=0, sun_dec=90)
assert np.allclose((att2.ra, att2.dec), (10, 45))


def test_apply_sun_pitch_yaw_with_grid():
"""Use np.ogrid to make a grid of RA/Dec values (via dpitches and dyaws)"""
dpitches, dyaws = np.ogrid[0:-3:2j, -5:5:3j]
atts = apply_sun_pitch_yaw(att=[0, 45, 10], pitch=dpitches, yaw=dyaws, sun_ra=0, sun_dec=90)
assert atts.shape == (2, 3)
exp = np.array(
[[[355., 45., 10.],
[360., 45., 10.],
[5., 45., 10.]],
[[355., 48., 10.],
[0., 48., 10.],
[5., 48., 10.]]])
assert np.allclose(atts.equatorial, exp)


def test_get_sun_pitch_yaw():
"""Test that values approximately match those from ORviewer.
See slack discussion "ORviewer sun / anti-sun plots azimuthal Sun yaw angle"
"""
pitch, yaw = get_sun_pitch_yaw(109, 55.3, time='2021:242')
assert np.allclose((pitch, yaw), (60.453385, 29.880125))
pitch, yaw = get_sun_pitch_yaw(238.2, -58.9, time='2021:242')
assert np.allclose((pitch, yaw), (92.405603, 210.56582))
pitch, yaw = get_sun_pitch_yaw(338, -9.1, time='2021:242')
assert np.allclose((pitch, yaw), (179.417797, 259.703451))
16 changes: 2 additions & 14 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
:mod:`Ska.Sun`
======================

.. automodule:: Ska.Sun


Functions
----------

.. autofunction:: position
.. autofunction:: pitch
.. autofunction:: nominal_roll
.. autofunction:: sph_dist




.. automodule:: Ska.Sun.Sun
:members:
17 changes: 11 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from setuptools import setup

try:
from testr.setup_helper import cmdclass
except ImportError:
cmdclass = {}

setup(name='Ska.Sun',
author = 'Jean Connelly',
author='Jean Connelly',
description='Sun position calculator',
author_email = 'jeanconn@head.cfa.harvard.edu',
py_modules = ['Ska.Sun'],
author_email='jconnelly@cfa.harvard.edu',
use_scm_version=True,
setup_requires=['setuptools_scm', 'setuptools_scm_git_archive'],
zip_safe=False,
packages=['Ska'],
package_dir={'Ska' : 'Ska'},
package_data={}
packages=['Ska', 'Ska.Sun', 'Ska.Sun.tests'],
package_data={},
tests_require=['pytest'],
cmdclass=cmdclass,
)

0 comments on commit 2b70301

Please sign in to comment.