Skip to content

Commit 2662051

Browse files
committed
reveal aer2eci, cleanup typing, add lat/lon sanity checks
add add'l test coverage include tests
1 parent ca3320e commit 2662051

File tree

15 files changed

+229
-185
lines changed

15 files changed

+229
-185
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
include LICENSE
2+
recursive-include tests *.py

README.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,10 @@
2020

2121
PyMap3D is intended for non-interactive use on massively parallel (HPC) and embedded systems.
2222
Includes some relevant
23-
[Vallado's algorithms](http://www.smad.com/vallado/fortran/fortran.html).
23+
[Vallado algorithms](http://www.smad.com/vallado/fortran/fortran.html).
2424

2525
[API docs](https://www.scivision.co/pymap3d)
2626

27-
Why not [PyProj](https://github.com/jswhit/pyproj)?
28-
29-
- PyMap3D does not require anything beyond pure Python + Numpy.
30-
- PyMap3D API is similar to Matlab Mapping Toolbox, while PyProj's interface is quite distinct
31-
- PyMap3D intrinsically handles local coordinate systems such as ENU,
32-
while for PyProj ENU requires some [additional
33-
effort](https://github.com/jswhit/pyproj/issues/105).
34-
- PyProj is oriented towards points on the planet surface, while
35-
PyMap3D handles points on or above the planet surface equally well,
36-
particularly important for airborne vehicles and remote sensing.
37-
3827
## Prerequisites
3928

4029
* Python ≥ 3.5 or PyPy3
@@ -96,7 +85,7 @@ converted to the desired coordinate system:
9685

9786
aer2ecef aer2enu aer2geodetic aer2ned
9887
ecef2aer ecef2enu ecef2enuv ecef2geodetic ecef2ned ecef2nedv
99-
ecef2eci eci2ecef
88+
ecef2eci eci2ecef eci2aer aer2eci
10089
enu2aer enu2ecef enu2geodetic
10190
geodetic2aer geodetic2ecef geodetic2enu geodetic2ned
10291
ned2aer ned2ecef ned2geodetic
@@ -108,7 +97,7 @@ converted to the desired coordinate system:
10897

10998
Additional functions:
11099

111-
* `loxodrome_inverse`: rhumb line distance and azimuth between ellipsoid points (lat,lon) akin to Matlab `distance('rh', ...)` and `azimuth('rh', ...)`
100+
`loxodrome_inverse`: rhumb line distance and azimuth between ellipsoid points (lat,lon) akin to Matlab `distance('rh', ...)` and `azimuth('rh', ...)`
112101

113102

114103
Abbreviations:
@@ -126,3 +115,16 @@ Abbreviations:
126115
Would need to update code to add these input parameters (just start a GitHub Issue to request).
127116
* Planetary perturbations and nutation etc. not fully considered.
128117

118+
## Notes
119+
120+
As compared to [PyProj](https://github.com/jswhit/pyproj):
121+
122+
- PyMap3D does not require anything beyond pure Python + Numpy.
123+
- PyMap3D API is similar to Matlab Mapping Toolbox, while PyProj's interface is quite distinct
124+
- PyMap3D intrinsically handles local coordinate systems such as ENU,
125+
while for PyProj ENU requires some [additional
126+
effort](https://github.com/jswhit/pyproj/issues/105).
127+
- PyProj is oriented towards points on the planet surface, while
128+
PyMap3D handles points on or above the planet surface equally well,
129+
particularly important for airborne vehicles and remote sensing.
130+

angle_distance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
2-
from numpy.testing import assert_allclose
32
from pymap3d.haversine import anglesep, anglesep_meeus
43
from argparse import ArgumentParser
4+
from pytest import approx
55

66

77
def main():
@@ -17,7 +17,7 @@ def main():
1717

1818
print('{:.6f} deg sep'.format(dist_deg))
1919

20-
assert_allclose(dist_deg, dist_deg_astropy)
20+
assert dist_deg == approx(dist_deg_astropy)
2121

2222

2323
if __name__ == '__main__':

pymap3d/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
from .ecef import geodetic2ecef, ecef2geodetic, eci2geodetic, Ellipsoid, ecef2enuv, enu2ecef, ecef2enu, enu2uvw, uvw2enu # noqa: F401
1414
from .ned import ned2ecef, ned2geodetic, geodetic2ned, ecef2nedv, ned2aer, aer2ned, ecef2ned # noqa: F401
1515
from .enu import enu2geodetic, geodetic2enu, aer2enu, enu2aer # noqa: F401
16-
from .aer import ecef2aer, aer2ecef, geodetic2aer, aer2geodetic, eci2aer # noqa: F401
16+
from .aer import ecef2aer, aer2ecef, geodetic2aer, aer2geodetic, eci2aer, aer2eci # noqa: F401
1717
from .los import lookAtSpheroid # noqa: F401
1818
from .lox import isometric, meridian_dist, loxodrome_inverse # noqa: F401

pymap3d/aer.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Tuple, Sequence
1+
from typing import Tuple
22
from datetime import datetime
33
import numpy as np
44
from .ecef import ecef2enu, geodetic2ecef, ecef2geodetic, enu2uvw
@@ -54,7 +54,8 @@ def geodetic2aer(lat: float, lon: float, h: float,
5454

5555

5656
def aer2geodetic(az: float, el: float, srange: float,
57-
lat0: float, lon0: float, h0: float, deg: bool=True) -> Tuple[float, float, float]:
57+
lat0: float, lon0: float, h0: float,
58+
deg: bool=True) -> Tuple[float, float, float]:
5859
"""
5960
Input:
6061
-----
@@ -75,8 +76,9 @@ def aer2geodetic(az: float, el: float, srange: float,
7576
return ecef2geodetic(x, y, z, deg=deg)
7677

7778

78-
def eci2aer(eci: Sequence[float], lat0: float, lon0: float, h0: float,
79-
t: Sequence[datetime]) -> Tuple[float, float, float]:
79+
def eci2aer(eci: Tuple[float, float, float],
80+
lat0: float, lon0: float, h0: float,
81+
t: datetime) -> Tuple[float, float, float]:
8082
"""
8183
Observer => Point
8284
@@ -93,7 +95,7 @@ def eci2aer(eci: Sequence[float], lat0: float, lon0: float, h0: float,
9395
azimuth, elevation (degrees/radians) [0,360),[0,90]
9496
slant range [meters] [0,Infinity)
9597
"""
96-
ecef = eci2ecef(eci, t)
98+
ecef = np.atleast_2d(eci2ecef(eci, t))
9799

98100
return ecef2aer(ecef[:, 0], ecef[:, 1], ecef[:, 2], lat0, lon0, h0)
99101

pymap3d/azelradec.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
"""
22
Azimuth / elevation <==> Right ascension, declination
33
"""
4-
from typing import Tuple, Union
4+
from typing import Tuple
55
from datetime import datetime
66
import numpy as np
7-
from .timeconv import str2dt
7+
from .vallado import azel2radec as vazel2radec, radec2azel as vradec2azel
88
try:
99
from astropy.time import Time
1010
from astropy import units as u
1111
from astropy.coordinates import Angle, SkyCoord, EarthLocation, AltAz, ICRS
1212
except ImportError:
13-
from .vallado import vazel2radec, vradec2azel
1413
Time = None
1514

1615

1716
def azel2radec(az_deg: float, el_deg: float,
1817
lat_deg: float, lon_deg: float,
19-
time: Union[str, datetime]) -> Tuple[float, float]:
20-
"""convert astronomical target horizontal azimuth, elevation to
21-
ecliptic right ascension, declination (degrees)
18+
time: datetime, usevallado: bool=False) -> Tuple[float, float]:
19+
"""
20+
viewing angle (az, el) to sky coordinates (ra, dec)
21+
22+
inputs
23+
------
24+
azimuth: degrees clockwize from North
25+
elevation: degrees above horizon (neglecting aberration)
26+
observer latitude [-90, 90], longitude [-180, 180] (degrees)
27+
time: datetime of observation
28+
29+
Outputs
30+
-------
31+
ecliptic right ascension, declination (degrees)
2232
"""
2333

24-
if Time is None: # non-AstroPy method, less accurate
34+
if usevallado or Time is None: # non-AstroPy method, less accurate
2535
return vazel2radec(az_deg, el_deg, lat_deg, lon_deg, time)
2636

27-
t = str2dt(time)
28-
2937
obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg)
3038

31-
direc = AltAz(location=obs, obstime=Time(t),
39+
direc = AltAz(location=obs, obstime=Time(time),
3240
az=az_deg * u.deg, alt=el_deg * u.deg)
3341

3442
sky = SkyCoord(direc.transform_to(ICRS()))
@@ -38,32 +46,37 @@ def azel2radec(az_deg: float, el_deg: float,
3846

3947
def radec2azel(ra_deg: float, dec_deg: float,
4048
lat_deg: float, lon_deg: float,
41-
time: Union[str, datetime]) -> Tuple[float, float]:
42-
"""convert astronomical target ecliptic right ascension, declination to
43-
horizontal azimuth, eelvation (degrees)
49+
time: datetime, usevallado: bool=False) -> Tuple[float, float]:
50+
"""
51+
sky coordinates (ra, dec) to viewing angle (az, el)
52+
53+
inputs
54+
------
55+
ecliptic right ascension, declination (degrees)
56+
observer latitude [-90, 90], longitude [-180, 180] (degrees)
57+
time: datetime of observation
58+
59+
Outputs
60+
-------
61+
azimuth: degrees clockwize from North
62+
elevation: degrees above horizon (neglecting aberration)
4463
"""
45-
if Time is None:
64+
65+
if usevallado or Time is None:
4666
return vradec2azel(ra_deg, dec_deg, lat_deg, lon_deg, time)
4767
# %% input trapping
48-
t = str2dt(time)
4968
lat = np.atleast_1d(lat_deg)
5069
lon = np.atleast_1d(lon_deg)
5170
ra = np.atleast_1d(ra_deg)
5271
dec = np.atleast_1d(dec_deg)
5372

54-
if not(lat.size == 1 & lon.size == 1):
55-
raise ValueError('radec2azel is designed for one observer and one or more points (ra,dec).')
56-
57-
if ra.shape != dec.shape:
58-
raise ValueError('ra and dec must be the same shape ndarray')
59-
6073
obs = EarthLocation(lat=lat * u.deg,
6174
lon=lon * u.deg)
6275

6376
points = SkyCoord(Angle(ra, unit=u.deg),
6477
Angle(dec, unit=u.deg),
6578
equinox='J2000.0')
6679

67-
altaz = points.transform_to(AltAz(location=obs, obstime=Time(t)))
80+
altaz = points.transform_to(AltAz(location=obs, obstime=Time(time)))
6881

6982
return altaz.az.degree, altaz.alt.degree

pymap3d/datetime2hourangle.py

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# Copyright (c) 2014-2018 Michael Hirsch, Ph.D.
22
from math import pi
3-
from typing import Union, List
43
from datetime import datetime
4+
import numpy as np
55
from .timeconv import str2dt
6-
#
7-
nan = float('nan')
86
try:
97
from astropy.time import Time
108
import astropy.units as u
@@ -18,9 +16,9 @@
1816
"""
1917

2018

21-
def datetime2sidereal(time: Union[str, datetime],
19+
def datetime2sidereal(time: datetime,
2220
lon_radians: float,
23-
usevallado: bool=True) -> Union[float, List[float]]:
21+
usevallado: bool=True) -> float:
2422
"""
2523
Convert ``datetime`` to sidereal time
2624
@@ -33,34 +31,33 @@ def datetime2sidereal(time: Union[str, datetime],
3331
lon
3432
longitude in RADIANS
3533
"""
34+
usevallado = usevallado or Time is None
3635
if usevallado:
37-
jd = datetime2julian(time)
36+
jd = datetime2julian(str2dt(time))
3837
# %% Greenwich Sidereal time RADIANS
39-
gst = julian2sidereal(jd)
38+
gst = np.atleast_1d(julian2sidereal(jd))
4039
# %% Algorithm 15 p. 188 rotate GST to LOCAL SIDEREAL TIME
41-
if isinstance(gst, float):
42-
tsr = gst + lon_radians # type: Union[float, List[float]]
43-
else:
44-
tsr = [g + lon_radians for g in gst]
45-
else: # astropy
46-
if Time is not None:
47-
tsr = Time(time).sidereal_time(kind='apparent',
48-
longitude=Longitude(lon_radians, unit=u.radian)).radian
49-
else:
50-
return datetime2sidereal(time, lon_radians, True)
40+
tsr = gst + lon_radians
41+
else:
42+
tsr = Time(time).sidereal_time(kind='apparent',
43+
longitude=Longitude(lon_radians, unit=u.radian)).radian
5144

5245
return tsr
5346

5447

55-
def datetime2julian(time: Union[str, datetime, List[datetime]]) -> Union[float, List[float]]:
48+
def datetime2julian(time: datetime) -> float:
5649
"""
5750
Python datetime to Julian time
5851
5952
from D.Vallado Fundamentals of Astrodynamics and Applications p.187
6053
and J. Meeus Astronomical Algorithms 1991 Eqn. 7.1 pg. 61
6154
"""
6255

63-
def _dt2julian(t: datetime) -> float:
56+
times = np.atleast_1d(time)
57+
assert times.ndim == 1
58+
59+
jd = np.empty(times.size)
60+
for i, t in enumerate(times):
6461
if t.month < 3:
6562
year = t.year - 1
6663
month = t.month + 12
@@ -72,22 +69,13 @@ def _dt2julian(t: datetime) -> float:
7269
B = 2 - A + int(A / 4.)
7370
C = ((t.second / 60. + t.minute) / 60. + t.hour) / 24.
7471

75-
jd = (int(365.25 * (year + 4716)) +
76-
int(30.6001 * (month + 1)) + t.day + B - 1524.5 + C)
77-
78-
return jd
72+
jd[i] = (int(365.25 * (year + 4716)) +
73+
int(30.6001 * (month + 1)) + t.day + B - 1524.5 + C)
7974

80-
time = str2dt(time)
75+
return jd.squeeze()
8176

82-
assert isinstance(time, datetime) or isinstance(time[0], datetime)
8377

84-
if isinstance(time, datetime):
85-
return _dt2julian(time)
86-
else:
87-
return [_dt2julian(t) for t in time]
88-
89-
90-
def julian2sidereal(juliandate: Union[float, List[float]]) -> Union[float, List[float]]:
78+
def julian2sidereal(juliandate: float) -> float:
9179
"""
9280
Convert Julian time to sidereal time
9381
@@ -100,7 +88,11 @@ def julian2sidereal(juliandate: Union[float, List[float]]) -> Union[float, List[
10088
10189
"""
10290

103-
def _jd2sr(jd: float):
91+
jdate = np.atleast_1d(juliandate)
92+
assert jdate.ndim == 1
93+
94+
tsr = np.empty(jdate.size)
95+
for i, jd in enumerate(jdate):
10496
# %% Vallado Eq. 3-42 p. 184, Seidelmann 3.311-1
10597
tUT1 = (jd - 2451545.0) / 36525.
10698

@@ -109,11 +101,6 @@ def _jd2sr(jd: float):
109101
tUT1 + 0.093104 * tUT1**2 - 6.2e-6 * tUT1**3)
110102

111103
# 1/86400 and %(2*pi) implied by units of radians
112-
tsr = gmst_sec * (2 * pi) / 86400. % (2 * pi)
104+
tsr[i] = gmst_sec * (2 * pi) / 86400. % (2 * pi)
113105

114-
return tsr
115-
116-
if isinstance(juliandate, float):
117-
return _jd2sr(juliandate)
118-
else:
119-
return [_jd2sr(jd) for jd in juliandate]
106+
return tsr.squeeze()

0 commit comments

Comments
 (0)