Skip to content

Commit

Permalink
Add a test, get tests passing, allow vectorized position_fast
Browse files Browse the repository at this point in the history
  • Loading branch information
taldcroft committed Nov 30, 2023
1 parent 896ad63 commit b9d11cd
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 111 deletions.
16 changes: 2 additions & 14 deletions ska_sun/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,9 @@ class Conf(ConfigNamespace):
"""

sun_position_method_default = ConfigItem(
["fast_and_accurate", "fast", "accurate"],
["accurate", "fast"],
"Default value of `method` parameter in ska_sun.position()"
' (default="fast_and_accurate").',
)

fast_and_accurate_pitch_limit = ConfigItem(
165.0,
"Pitch value above which the accurate method is used for "
"ska_sun.pitch() and ska_sun.off_nom_roll() when method='fast_and_accurate'.",
)

from_chandra_default = ConfigItem(
False,
"Default value of `from_chandra` parameter in ska_sun.position_accurate() "
"(default=False).",
' (default="accurate").',
)


Expand Down
114 changes: 23 additions & 91 deletions ska_sun/sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
"""
Utility for calculating sun position, pitch angle and values related to roll.
"""
import functools
from math import acos, asin, atan2, cos, degrees, pi, radians, sin

import numba
import numpy as np
from astropy.table import Table
from chandra_aca.planets import get_planet_chandra, get_planet_eci
from chandra_aca.transform import eci_to_radec, radec_to_eci
from cxotime import convert_time_format
from numpy import arccos as acos
from numpy import arcsin as asin
from numpy import arctan2 as atan2
from numpy import cos, degrees, pi, radians, sin
from Quaternion import Quat
from ska_helpers import chandra_models

Expand Down Expand Up @@ -38,19 +39,6 @@ def _roll_table_read_func(filename):
return Table.read(filename), filename


def fill_kwargs_from_conf(items, conf=conf):
def wrap_outer(func):
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
for name, item in items.items():
kwargs.setdefault(name, getattr(conf, item))
return func(*args, **kwargs)

return wrapped_func

return wrap_outer


@chandra_models.chandra_models_cache
def load_roll_table():
"""Load the pitch/roll table from the chandra_models repo.
Expand Down Expand Up @@ -131,8 +119,7 @@ def allowed_rolldev(pitch, roll_table=None):
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


@functools.lru_cache(maxsize=8)
def position_fast(time, **kwargs):
def position_fast(time):
"""
Calculate the sun position at the given ``time`` using a fast approximation.
Expand Down Expand Up @@ -178,8 +165,8 @@ def position_fast_at_jd(jd):
Parameters
----------
time : CxoTimeLike (scalar)
Input time.
jd : float
Input time in JD.
Returns
-------
Expand Down Expand Up @@ -264,17 +251,12 @@ def position_fast_at_jd(jd):
return ra / dtor, dec / dtor


@fill_kwargs_from_conf({"from_chandra": "from_chandra_default"})
@functools.lru_cache(maxsize=8)
def position_accurate(time, **kwargs):
def position_accurate(time, from_chandra=False):
"""
Calculate the sun RA, Dec at the given ``time`` from Earth geocenter or Chandra.
The default for ``from_chandra`` is set by ``ska_sun.conf.from_chandra_default``,
which defaults to ``False``.
If ``from_chandra=False`` the position is calculated from Earth geocenter. If
``from_chandra=True`` the position is calculated from Chandra using the Chandra
If ``from_chandra=False`` (default) the position is calculated from Earth geocenter.
If ``from_chandra=True`` the position is calculated from Chandra using the Chandra
predictive ephemeris via the cheta telemetry archive.
These methods rely on the DE432 ephemeris and functions in ``chandra_aca.planets``.
Expand All @@ -301,72 +283,22 @@ def position_accurate(time, **kwargs):
sun_dec : float
Declination in decimal degrees (J2000).
"""
from_chandra = kwargs["from_chandra"]

func = get_planet_chandra if from_chandra else get_planet_eci
eci_sun = func("sun", time)
ra, dec = eci_to_radec(eci_sun)
return ra, dec


@fill_kwargs_from_conf({"from_chandra": "from_chandra_default"})
@functools.lru_cache(maxsize=8)
def position_fast_and_accurate(
time,
**kwargs,
):
"""
Calculate sun RA, Dec at `time`` using a combination of fast and accurate methods.
If ``ra`` and ``dec`` are provided then the sun pitch angle to that sky position is
calculated. If the pitch is greater than
``ska_sun.conf.fast_and_accurate_pitch_limit`` (default 165 deg) then the accurate
method is used. Otherwise the fast method is used.
Parameters
----------
time : CxoTimeLike
Time at which to calculate the Sun's position.
targ_ra : (float, optional)
Right ascension of a target object. Defaults to None.
targ_dec : (float, optional)
Declination of a target object. Defaults to None.
from_chandra : bool, optional
If True compute position from Chandra using cheta ephemeris. Defaults to None.
Returns
-------
sun_ra : float
Right ascension of the Sun.
sun_dec : float
Declination of the Sun.
"""
targ_ra = kwargs.get("targ_ra")
targ_dec = kwargs.get("targ_dec")
from_chandra = kwargs.get("from_chandra")

sun_ra, sun_dec = position_fast(time)

if targ_ra is not None and targ_dec is not None:
pitch = sph_dist(targ_ra, targ_dec, sun_ra, sun_dec)
if pitch > conf.fast_and_accurate_pitch_limit:
sun_ra, sun_dec = position_accurate(time, from_chandra=from_chandra)

return sun_ra, sun_dec


def position(time, method=None, **kwargs):
"""
Calculate the sun RA, Dec at the given ``time`` from Earth geocenter or Chandra.
``method`` sets the method for computing the sun position which is used for pitch.
The default is set by ``ska_sun.conf.sun_pitch_roll_method_default``, which defaults
to ``fast_and_accurate``. The available options are:
to ``accurate``. The available options are:
- ``fast_and_accurate``: maintain ``off_nominal_roll`` accuracy to < 2 deg (see
``position_fast_and_accurate()``).
- ``fast``: Use the fast method (see ``position_fast()``).
- ``accurate``: Use the accurate method (see ``position_accurate()``).
- ``fast``: Use the fast method (see ``position_fast()``).
Examples
--------
Expand Down Expand Up @@ -438,7 +370,7 @@ def sph_dist(a1, d1, a2, d2):
d1 = radians(d1)
a2 = radians(a2)
d2 = radians(d2)
val = cos(d1) * cos(d2) * cos(a1 - a2) + sin(d1) * sin(d2)
val = cos(d1) * cos(d2) * cos(a1 - a2) + np.sin(d1) * np.sin(d2)
if val > 1.0:
val = 1.0
elif val < -1.0:
Expand Down Expand Up @@ -468,9 +400,8 @@ def pitch(ra, dec, time=None, sun_ra=None, sun_dec=None, method=None):
Sun RA (deg) instead of time.
sun_dec : float, optional
Sun Dec (deg) instead of time.
method : str, optional
Method for calculating sun position. Valid options are "fast_and_accurate",
"fast", and "accurate".
method : str, optional.
Method for calculating sun position. Valid options are "accurate", "fast".
Returns
-------
Expand All @@ -483,7 +414,7 @@ def pitch(ra, dec, time=None, sun_ra=None, sun_dec=None, method=None):
96.256434327840864
"""
if time is not None:
sun_ra, sun_dec = position(time, method=method, targ_ra=ra, targ_dec=dec)
sun_ra, sun_dec = position(time, method=method)

pitch = sph_dist(ra, dec, sun_ra, sun_dec)
return pitch
Expand All @@ -505,7 +436,7 @@ def _radec2eci(ra, dec):
return np.array([np.cos(r) * np.cos(d), np.sin(r) * np.cos(d), np.sin(d)])


def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None):
def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None, method=None):
"""
Calculate the nominal roll angle for the given spacecraft attitude.
Expand All @@ -518,12 +449,14 @@ def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None):
Right ascension.
dec : float
Declination.
time : str, optional
time : CxoTimeLike, optional
Time in any Chandra.Time format.
sun_ra : float, optional
Sun right ascension (instead of using `time`).
sun_dec : float, optional
Sun declination (instead of using `time`).
method : str, optional.
Method for calculating sun position. Valid options are "accurate", "fast".
Returns
-------
Expand All @@ -536,7 +469,7 @@ def nominal_roll(ra, dec, time=None, sun_ra=None, sun_dec=None):
68.830209134280665 # vs. 68.80 for obsid 12393 in JAN1711A
"""
if time is not None:
sun_ra, sun_dec = position(time)
sun_ra, sun_dec = position(time, method=method)
roll = _nominal_roll(ra, dec, sun_ra, sun_dec)
return roll

Expand Down Expand Up @@ -589,8 +522,7 @@ def off_nominal_roll(att, time=None, sun_ra=None, sun_dec=None, method=None):
sun_dec : float, optional
Sun Dec (deg) instead of time.
method : str, optional
Method for calculating sun position. Valid options are "fast_and_accurate",
"fast", and "accurate".
Method for calculating sun position. Valid options are "accurate", "fast".
Returns
-------
Expand All @@ -606,7 +538,7 @@ def off_nominal_roll(att, time=None, sun_ra=None, sun_dec=None, method=None):
ra, dec, roll = q.equatorial

if time is not None:
sun_ra, sun_dec = position(time, method=method, targ_ra=ra, targ_dec=dec)
sun_ra, sun_dec = position(time, method=method)

nom_roll = _nominal_roll(ra, dec, sun_ra, sun_dec)
off_nom_roll = roll - nom_roll
Expand Down
41 changes: 35 additions & 6 deletions ska_sun/tests/test_sun.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

import astropy.units as u
import numpy as np
import pytest
from cxotime import CxoTime
from Quaternion import Quat

import ska_sun
Expand All @@ -14,6 +17,12 @@
from ska_sun import pitch as sun_pitch
from ska_sun import position


@pytest.fixture()
def fast_sun_position_method(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(ska_sun.conf, "sun_position_method_default", "fast")


# Expected pitch, rolldev pairs
exp_pitch_rolldev = np.array(
[
Expand Down Expand Up @@ -66,7 +75,7 @@ def test_duplicate_pitch_rolldev(monkeypatch):
assert np.isclose(allowed_rolldev(pitch_max + 1e-8), -1.0, rtol=0, atol=1e-6)


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

Expand Down Expand Up @@ -113,20 +122,21 @@ def test_position_diff_methods():
# which should be the reference giving the exp_pitch anyway
ra_slow, dec_slow = position(time, method="accurate", from_chandra=True)
sun_pitch_slow = ska_sun.pitch(targ_ra, targ_dec, sun_ra=ra_slow, sun_dec=dec_slow)
assert np.isclose(sun_pitch_slow, exp_pitch, rtol=0, atol=2e-5)
# Good within 3 arcsec
assert np.isclose(sun_pitch_slow, exp_pitch, rtol=0, atol=3 / 3600)


def test_nominal_roll():
def test_nominal_roll(fast_sun_position_method):
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_nominal_roll_range():
def test_nominal_roll_range(fast_sun_position_method):
roll = nominal_roll(0, 89.9, time="2019:006:12:00:00")
assert np.allclose(roll, 287.24879) # range in 0-360 and value for sparkles test


def test_off_nominal_roll_and_pitch():
def test_off_nominal_roll_and_pitch(fast_sun_position_method):
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)
Expand Down Expand Up @@ -175,7 +185,7 @@ def test_apply_sun_pitch_yaw_with_grid():
assert np.allclose(atts.equatorial, exp)


def test_get_sun_pitch_yaw():
def test_get_sun_pitch_yaw(fast_sun_position_method):
"""Test that values approximately match those from ORviewer.
See slack discussion "ORviewer sun / anti-sun plots azimuthal Sun yaw angle"
Expand Down Expand Up @@ -208,3 +218,22 @@ def test_roll_table_pitch_increasing():
"""
dat = ska_sun.load_roll_table()
assert np.all(np.diff(dat["pitch"]) >= 0)


@pytest.mark.parametrize("method", ["fast", "accurate"])
def test_array_input_and_different_formats(method):
date0 = CxoTime("2019:001")
dates = np.array([date0 + i_dt * u.day for i_dt in np.arange(0, 10, 1)])
times = [date.secs for date in dates]
# Make sure list, array, CxoTime array inputs work
pos1 = ska_sun.position(times, method=method)
pos2 = ska_sun.position(dates, method=method)
pos3 = ska_sun.position(CxoTime(dates), method=method)
for idx in range(2):
assert np.all(pos1[idx] == pos2[idx])
assert np.all(pos1[idx] == pos3[idx])

for ra, dec, time in zip(pos1[0], pos1[1], times):
ra2, dec2 = ska_sun.position(time, method=method)
assert ra == ra2
assert dec == dec2

0 comments on commit b9d11cd

Please sign in to comment.