diff --git a/.travis.yml b/.travis.yml index b175d28..3856650 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ git: python: - 3.6 - 3.5 -- pypy3 os: - linux diff --git a/README.md b/README.md index fbb736b..548301b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Includes some relevant ## Prerequisites * Python ≥ 3.5 or PyPy3 -* [AstroPy](http://www.astropy.org/) (optional, used for ECI coordinates) + +References to AstroPy are optional, algorithms from Vallado and Meeus are used if AstroPy is not present. ## Install @@ -39,7 +40,7 @@ pip install pymap3d or for the latest development code: ```sh git clone https://github.com/scivision/pymap3d - +cd pymap3d pip install -e . ``` diff --git a/pymap3d/aer.py b/pymap3d/aer.py index e60ccc8..55250d6 100644 --- a/pymap3d/aer.py +++ b/pymap3d/aer.py @@ -78,7 +78,8 @@ def aer2geodetic(az: float, el: float, srange: float, def eci2aer(eci: Tuple[float, float, float], lat0: float, lon0: float, h0: float, - t: datetime) -> Tuple[float, float, float]: + t: datetime, + useastropy: bool=True) -> Tuple[float, float, float]: """ Observer => Point @@ -95,14 +96,15 @@ def eci2aer(eci: Tuple[float, float, float], azimuth, elevation (degrees/radians) [0,360),[0,90] slant range [meters] [0,Infinity) """ - ecef = np.atleast_2d(eci2ecef(eci, t)) + ecef = np.atleast_2d(eci2ecef(eci, t, useastropy)) return ecef2aer(ecef[:, 0], ecef[:, 1], ecef[:, 2], lat0, lon0, h0) def aer2eci(az: float, el: float, srange: float, lat0: float, lon0: float, h0: float, t: datetime, - ell=None, deg: bool=True) -> np.ndarray: + ell=None, deg: bool=True, + useastropy: bool=True) -> np.ndarray: """ input @@ -120,7 +122,7 @@ def aer2eci(az: float, el: float, srange: float, """ x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell, deg) - return ecef2eci(np.column_stack((x, y, z)), t) + return ecef2eci(np.column_stack((x, y, z)), t, useastropy) def aer2ecef(az: float, el: float, srange: float, diff --git a/pymap3d/azelradec.py b/pymap3d/azelradec.py index 2c5d993..dff87e0 100644 --- a/pymap3d/azelradec.py +++ b/pymap3d/azelradec.py @@ -5,6 +5,7 @@ from datetime import datetime import numpy as np from .vallado import azel2radec as vazel2radec, radec2azel as vradec2azel +from .timeconv import str2dt # astropy can't handle xarray times (yet) try: from astropy.time import Time from astropy import units as u @@ -36,7 +37,7 @@ def azel2radec(az_deg: float, el_deg: float, obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg) - direc = AltAz(location=obs, obstime=Time(time), + direc = AltAz(location=obs, obstime=Time(str2dt(time)), az=az_deg * u.deg, alt=el_deg * u.deg) sky = SkyCoord(direc.transform_to(ICRS())) @@ -77,6 +78,6 @@ def radec2azel(ra_deg: float, dec_deg: float, Angle(dec, unit=u.deg), equinox='J2000.0') - altaz = points.transform_to(AltAz(location=obs, obstime=Time(time))) + altaz = points.transform_to(AltAz(location=obs, obstime=Time(str2dt(time)))) return altaz.az.degree, altaz.alt.degree diff --git a/pymap3d/ecef.py b/pymap3d/ecef.py index b34ec90..9952500 100644 --- a/pymap3d/ecef.py +++ b/pymap3d/ecef.py @@ -236,7 +236,8 @@ def uvw2enu(u: float, v: float, w: float, return East, North, Up -def eci2geodetic(eci: np.ndarray, t: datetime) -> Tuple[float, float, float]: +def eci2geodetic(eci: np.ndarray, t: datetime, + useastropy: bool=True) -> Tuple[float, float, float]: """ convert ECI to geodetic coordinates @@ -257,7 +258,7 @@ def eci2geodetic(eci: np.ndarray, t: datetime) -> Tuple[float, float, float]: eci2geodetic() a.k.a. eci2lla() """ - ecef = np.atleast_2d(eci2ecef(eci, t)) + ecef = np.atleast_2d(eci2ecef(eci, t, useastropy)) return np.asarray(ecef2geodetic(ecef[:, 0], ecef[:, 1], ecef[:, 2])).squeeze() diff --git a/pymap3d/eci.py b/pymap3d/eci.py index 21ae0c6..3659fa8 100644 --- a/pymap3d/eci.py +++ b/pymap3d/eci.py @@ -1,5 +1,6 @@ from datetime import datetime import numpy as np +from .datetime2hourangle import datetime2sidereal try: from astropy.time import Time except ImportError as e: @@ -7,7 +8,8 @@ def eci2ecef(eci: np.ndarray, - time: datetime) -> np.ndarray: + time: datetime, + useastropy: bool=True) -> np.ndarray: """ Observer => Point @@ -20,10 +22,13 @@ def eci2ecef(eci: np.ndarray, ------ x,y,z [meters] target ECEF location [0,Infinity) """ - if Time is None: - raise ImportError('eci2ecef requires Numpy and AstroPy') + useastropy = useastropy and Time + + if useastropy: + gst = Time(time).sidereal_time('apparent', 'greenwich').radian + else: + gst = datetime2sidereal(time, 0.) - gst = Time(time).sidereal_time('apparent', 'greenwich').radian gst = np.atleast_1d(gst) assert gst.ndim == 1 and isinstance(gst[0], float) # must be in radians! @@ -43,7 +48,8 @@ def eci2ecef(eci: np.ndarray, def ecef2eci(ecef: np.ndarray, - time: datetime) -> np.ndarray: + time: datetime, + useastropy: bool=True) -> np.ndarray: """ Point => Point @@ -57,10 +63,13 @@ def ecef2eci(ecef: np.ndarray, ------ eci x,y,z (meters) """ - if Time is None: - raise ImportError('ecef2eci requires AstroPy') + useastropy = useastropy and Time + + if useastropy: + gst = Time(time).sidereal_time('apparent', 'greenwich').radian + else: + gst = datetime2sidereal(time, 0.) - gst = Time(time).sidereal_time('apparent', 'greenwich').radian gst = np.atleast_1d(gst) assert gst.ndim == 1 and isinstance(gst[0], float) # must be in radians! diff --git a/pymap3d/timeconv.py b/pymap3d/timeconv.py index fe14649..29a2cfc 100644 --- a/pymap3d/timeconv.py +++ b/pymap3d/timeconv.py @@ -14,7 +14,8 @@ def str2dt(time: datetime) -> np.ndarray: return time elif isinstance(time, str): return parse(time) - elif isinstance(time[0], str): - return [parse(t) for t in time] - else: - return time.values.astype('datetime64[us]').astype(datetime) + else: # some sort of iterable + try: + return [parse(t) for t in time] + except TypeError: + return time.values.astype('datetime64[us]').astype(datetime) diff --git a/setup.cfg b/setup.cfg index 6d120a8..4ae5f3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pymap3d -version = 1.7.9 +version = 1.7.10 author = Michael Hirsch, Ph.D. author_email = scivision@users.noreply.github.com description = pure Python coordinate conversions, following convention of several popular Matlab routines. diff --git a/tests/test_astropy.py b/tests/test_astropy.py index 5c4e797..56302f8 100755 --- a/tests/test_astropy.py +++ b/tests/test_astropy.py @@ -39,7 +39,7 @@ def test_anglesep_meeus(): assert pmh.anglesep_meeus(35, 23, 84, 20) == approx(ha) -def test_eci(): +def test_eci_astropy(): pytest.importorskip('astropy') t = '2013-01-15T12:00:05' @@ -60,7 +60,7 @@ def test_eci(): pm.aer2eci(aer1[0], aer1[1], -1, 42, -100, 0, t) -def test_eci_times(): +def test_eci_times_astropy(): pytest.importorskip('astropy') with pytest.raises(AssertionError): @@ -73,5 +73,36 @@ def test_eci_times(): assert pm.ecef2eci(pm.eci2ecef(eci0s, [t0] * 2), [t0] * 2) == approx(eci0s) +def test_eci_vallado(): + t = '2013-01-15T12:00:05' + lla = pm.eci2geodetic(eci0, t, useastropy=False) + assert lla == approx(lla0, rel=0.2) + + eci1 = pm.eci2ecef(eci0, t, useastropy=False) + assert eci1 == approx([649012.04640917, -4697980.55129606, 4250818.82815207], rel=0.001) + + assert pm.ecef2eci(eci1, t, useastropy=False) == approx(eci0, rel=0.001) + + aer1 = pm.eci2aer(eci0, 42, -100, 0, t, useastropy=False) + assert aer1 == approx([83.73050, -6.614478, 1.473510e6], rel=0.001) + + assert pm.aer2eci(*aer1, 42, -100, 0, t, useastropy=False) == approx(eci0, rel=0.001) + + with pytest.raises(ValueError): + pm.aer2eci(aer1[0], aer1[1], -1, 42, -100, 0, t, useastropy=False) + + +def test_eci_times_vallado(): + with pytest.raises(AssertionError): + pm.eci2ecef(eci0, [t0, t0], useastropy=False) + + with pytest.raises(AssertionError): + pm.ecef2eci(eci0, [t0, t0], useastropy=False) + + eci0s = np.stack((eci0, eci0)) + assert pm.ecef2eci(pm.eci2ecef(eci0s, [t0] * 2, useastropy=False), + [t0] * 2, useastropy=False) == approx(eci0s, rel=0.001) + + if __name__ == '__main__': pytest.main(['-xrsv', __file__]) diff --git a/tests/test_main.py b/tests/test_main.py index 10d16ce..0fa50ed 100755 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,10 +6,8 @@ """ import pytest from pytest import approx -from datetime import datetime import numpy as np import pymap3d as pm -from pymap3d.timeconv import str2dt pi = np.pi nan = np.nan @@ -46,15 +44,6 @@ def test_ellipsoid(): assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid('moon')) == approx([41.808706, -82., 4.630807e6]) -def test_str2dt(): - - assert str2dt(datetime(2014, 4, 6, 8)) == datetime(2014, 4, 6, 8) # passthrough - assert str2dt('2014-04-06T08:00:00') == datetime(2014, 4, 6, 8) - ti = [str2dt('2014-04-06T08:00:00'), str2dt('2014-04-06T08:01:02')] - to = [datetime(2014, 4, 6, 8), datetime(2014, 4, 6, 8, 1, 2)] - assert ti == to # even though ti is numpy array of datetime and to is list of datetime - - def test_losint(): pytest.importorskip('pytest', minversion='3.5') diff --git a/tests/test_time.py b/tests/test_time.py new file mode 100644 index 0000000..22ab908 --- /dev/null +++ b/tests/test_time.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +import pytest +from pymap3d.timeconv import str2dt +from datetime import datetime + +t0 = datetime(2014, 4, 6, 8) + + +def test_str2dt(): + assert str2dt(t0) == t0 # passthrough + assert str2dt('2014-04-06T08:00:00') == t0 + ti = [str2dt('2014-04-06T08:00:00'), str2dt('2014-04-06T08:01:02')] + to = [t0, datetime(2014, 4, 6, 8, 1, 2)] + assert ti == to # even though ti is numpy array of datetime and to is list of datetime + + +def test_xarray_time(): + xarray = pytest.importorskip('xarray') + + t = {'time': t0} + + ds = xarray.Dataset(t) + assert str2dt(ds['time']) == t0 + + +def test_pandas_time(): + pandas = pytest.importorskip('pandas') + + t = pandas.Series(t0) + assert str2dt(t) == t0 + + +if __name__ == '__main__': + pytest.main([__file__])