From b9d11cdc00f7037904879c73df4fcb79380d16da Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Thu, 30 Nov 2023 12:50:50 -0500 Subject: [PATCH] Add a test, get tests passing, allow vectorized position_fast --- ska_sun/config.py | 16 +----- ska_sun/sun.py | 114 ++++++++------------------------------ ska_sun/tests/test_sun.py | 41 ++++++++++++-- 3 files changed, 60 insertions(+), 111 deletions(-) diff --git a/ska_sun/config.py b/ska_sun/config.py index 65eb370..c5aa66f 100644 --- a/ska_sun/config.py +++ b/ska_sun/config.py @@ -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").', ) diff --git a/ska_sun/sun.py b/ska_sun/sun.py index 97b8fd4..cfd8ab9 100755 --- a/ska_sun/sun.py +++ b/ska_sun/sun.py @@ -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 @@ -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. @@ -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. @@ -178,8 +165,8 @@ def position_fast_at_jd(jd): Parameters ---------- - time : CxoTimeLike (scalar) - Input time. + jd : float + Input time in JD. Returns ------- @@ -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``. @@ -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 -------- @@ -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: @@ -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 ------- @@ -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 @@ -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. @@ -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 ------- @@ -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 @@ -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 ------- @@ -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 diff --git a/ska_sun/tests/test_sun.py b/ska_sun/tests/test_sun.py index d9006aa..df5aeec 100644 --- a/ska_sun/tests/test_sun.py +++ b/ska_sun/tests/test_sun.py @@ -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 @@ -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( [ @@ -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)) @@ -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) @@ -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" @@ -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