Skip to content

Commit bf34608

Browse files
committed
eci2ecef: scalar output for scalar input with Numpy backend
add test for Numpy and AstroPy backends fixes #98
1 parent da9291b commit bf34608

File tree

6 files changed

+141
-59
lines changed

6 files changed

+141
-59
lines changed

src/pymap3d/aer.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,9 @@ def eci2aer(x, y, z, lat0, lon0, h0, t: datetime, *, deg: bool = True) -> tuple:
199199
slant range [meters]
200200
"""
201201

202-
try:
203-
xecef, yecef, zecef = eci2ecef(x, y, z, t)
204-
except NameError:
205-
raise ImportError("pip install numpy")
202+
xe, ye, ze = eci2ecef(x, y, z, t)
206203

207-
return ecef2aer(xecef, yecef, zecef, lat0, lon0, h0, deg=deg)
204+
return ecef2aer(xe, ye, ze, lat0, lon0, h0, deg=deg)
208205

209206

210207
def aer2eci(

src/pymap3d/ecef.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ def geodetic2ecef(
7575
lon = radians(lon)
7676

7777
# radius of curvature of the prime vertical section
78-
N = ell.semimajor_axis**2 / hypot(
79-
ell.semimajor_axis * cos(lat), ell.semiminor_axis * sin(lat)
80-
)
78+
N = ell.semimajor_axis**2 / hypot(ell.semimajor_axis * cos(lat), ell.semiminor_axis * sin(lat))
8179
# Compute cartesian (geocentric) coordinates given (curvilinear) geodetic coordinates.
8280
x = (N + alt) * cos(lat) * cos(lon)
8381
y = (N + alt) * cos(lat) * sin(lon)
@@ -202,9 +200,7 @@ def ecef2geodetic(
202200

203201
# inside ellipsoid?
204202
inside = (
205-
x**2 / ell.semimajor_axis**2
206-
+ y**2 / ell.semimajor_axis**2
207-
+ z**2 / ell.semiminor_axis**2
203+
x**2 / ell.semimajor_axis**2 + y**2 / ell.semimajor_axis**2 + z**2 / ell.semiminor_axis**2
208204
< 1
209205
)
210206

src/pymap3d/eci.py

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from datetime import datetime
66

7-
import numpy as np
7+
import numpy
88

99
try:
1010
import astropy.units as u
@@ -45,36 +45,97 @@ def eci2ecef(x, y, z, time: datetime) -> tuple:
4545
"""
4646

4747
try:
48-
gcrs = GCRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=time)
49-
itrs = gcrs.transform_to(ITRS(obstime=time))
50-
51-
x_ecef = itrs.x.value
52-
y_ecef = itrs.y.value
53-
z_ecef = itrs.z.value
48+
return eci2ecef_astropy(x, y, z, time)
5449
except NameError:
55-
x = np.atleast_1d(x)
56-
y = np.atleast_1d(y)
57-
z = np.atleast_1d(z)
58-
gst = np.atleast_1d(greenwichsrt(juliandate(time)))
59-
assert (
60-
x.shape == y.shape == z.shape
61-
), f"shape mismatch: x: ${x.shape} y: {y.shape} z: {z.shape}"
62-
if gst.size == 1 and x.size != 1:
63-
gst = np.broadcast_to(gst, x.shape[0])
64-
assert x.size == gst.size, f"shape mismatch: x: {x.shape} gst: {gst.shape}"
65-
66-
eci = np.column_stack((x.ravel(), y.ravel(), z.ravel()))
67-
ecef = np.empty((x.size, 3))
68-
for i in range(eci.shape[0]):
69-
ecef[i, :] = R3(gst[i]) @ eci[i, :].T
70-
71-
x_ecef = ecef[:, 0].reshape(x.shape)
72-
y_ecef = ecef[:, 1].reshape(y.shape)
73-
z_ecef = ecef[:, 2].reshape(z.shape)
50+
return eci2ecef_numpy(x, y, z, time)
51+
52+
53+
def eci2ecef_astropy(x, y, z, t: datetime) -> tuple:
54+
"""
55+
eci2ecef using Astropy
56+
57+
Parameters
58+
----------
59+
x : float
60+
ECI x-location [meters]
61+
y : float
62+
ECI y-location [meters]
63+
z : float
64+
ECI z-location [meters]
65+
t : datetime.datetime
66+
time of obsevation (UTC)
67+
68+
Results
69+
-------
70+
x_ecef : float
71+
x ECEF coordinate
72+
y_ecef : float
73+
y ECEF coordinate
74+
z_ecef : float
75+
z ECEF coordinate
76+
"""
77+
78+
gcrs = GCRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=t)
79+
itrs = gcrs.transform_to(ITRS(obstime=t))
80+
81+
x_ecef = itrs.x.value
82+
y_ecef = itrs.y.value
83+
z_ecef = itrs.z.value
7484

7585
return x_ecef, y_ecef, z_ecef
7686

7787

88+
def eci2ecef_numpy(x, y, z, t: datetime) -> tuple:
89+
"""
90+
eci2ecef using Numpy
91+
92+
less accurate than astropy, but may be good enough.
93+
94+
Parameters
95+
----------
96+
x : float
97+
ECI x-location [meters]
98+
y : float
99+
ECI y-location [meters]
100+
z : float
101+
ECI z-location [meters]
102+
t : datetime.datetime
103+
time of obsevation (UTC)
104+
105+
Results
106+
-------
107+
x_ecef : float
108+
x ECEF coordinate
109+
y_ecef : float
110+
y ECEF coordinate
111+
z_ecef : float
112+
z ECEF coordinate
113+
"""
114+
115+
x = numpy.atleast_1d(x)
116+
y = numpy.atleast_1d(y)
117+
z = numpy.atleast_1d(z)
118+
gst = numpy.atleast_1d(greenwichsrt(juliandate(t)))
119+
assert (
120+
x.shape == y.shape == z.shape
121+
), f"shape mismatch: x: ${x.shape} y: {y.shape} z: {z.shape}"
122+
123+
if gst.size == 1 and x.size != 1:
124+
gst = numpy.broadcast_to(gst, x.shape[0])
125+
assert x.size == gst.size, f"shape mismatch: x: {x.shape} gst: {gst.shape}"
126+
127+
eci = numpy.column_stack((x.ravel(), y.ravel(), z.ravel()))
128+
ecef = numpy.empty((x.size, 3))
129+
for i in range(eci.shape[0]):
130+
ecef[i, :] = R3(gst[i]) @ eci[i, :].T
131+
132+
x_ecef = ecef[:, 0].reshape(x.shape)
133+
y_ecef = ecef[:, 1].reshape(y.shape)
134+
z_ecef = ecef[:, 2].reshape(z.shape)
135+
136+
return x_ecef.squeeze()[()], y_ecef.squeeze()[()], z_ecef.squeeze()[()]
137+
138+
78139
def ecef2eci(x, y, z, time: datetime) -> tuple:
79140
"""
80141
Point => Point ECEF => ECI
@@ -112,15 +173,15 @@ def ecef2eci(x, y, z, time: datetime) -> tuple:
112173
y_eci = eci.y.value
113174
z_eci = eci.z.value
114175
except NameError:
115-
x = np.atleast_1d(x)
116-
y = np.atleast_1d(y)
117-
z = np.atleast_1d(z)
118-
gst = np.atleast_1d(greenwichsrt(juliandate(time)))
176+
x = numpy.atleast_1d(x)
177+
y = numpy.atleast_1d(y)
178+
z = numpy.atleast_1d(z)
179+
gst = numpy.atleast_1d(greenwichsrt(juliandate(time)))
119180
assert x.shape == y.shape == z.shape
120181
assert x.size == gst.size
121182

122-
ecef = np.column_stack((x.ravel(), y.ravel(), z.ravel()))
123-
eci = np.empty((x.size, 3))
183+
ecef = numpy.column_stack((x.ravel(), y.ravel(), z.ravel()))
184+
eci = numpy.empty((x.size, 3))
124185
for i in range(x.size):
125186
eci[i, :] = R3(gst[i]).T @ ecef[i, :]
126187

@@ -133,4 +194,6 @@ def ecef2eci(x, y, z, time: datetime) -> tuple:
133194

134195
def R3(x: float):
135196
"""Rotation matrix for ECI"""
136-
return np.array([[np.cos(x), np.sin(x), 0], [-np.sin(x), np.cos(x), 0], [0, 0, 1]])
197+
return numpy.array(
198+
[[numpy.cos(x), numpy.sin(x), 0], [-numpy.sin(x), numpy.cos(x), 0], [0, 0, 1]]
199+
)

src/pymap3d/enu.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
""" transforms involving ENU East North Up """
2+
23
from __future__ import annotations
34

45
from math import tau

src/pymap3d/spherical.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
longitude, height) and geocentric spherical (spherical latitude, longitude,
44
radius).
55
"""
6+
67
from __future__ import annotations
78

89
from .ellipsoid import Ellipsoid

src/pymap3d/tests/test_eci.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
import datetime
22

33
import pymap3d as pm
44
import pytest
@@ -9,25 +9,51 @@
99
except ImportError:
1010
astropy = None
1111

12+
ECI = (-2981784, 5207055, 3161595)
13+
ECEF = [-5762640, -1682738, 3156028]
14+
UTC = datetime.datetime(2019, 1, 4, 12, tzinfo=datetime.timezone.utc)
15+
1216

1317
def test_eci2ecef():
1418
pytest.importorskip("numpy")
1519
# this example from Matlab eci2ecef docs
16-
eci = [-2981784, 5207055, 3161595]
17-
utc = datetime(2019, 1, 4, 12)
18-
ecef = pm.eci2ecef(*eci, utc)
20+
ecef = pm.eci2ecef(*ECI, UTC)
21+
22+
assert isinstance(ecef[0], float)
23+
assert isinstance(ecef[1], float)
24+
assert isinstance(ecef[2], float)
25+
26+
27+
def test_eci2ecef_numpy():
28+
pytest.importorskip("numpy")
29+
30+
ecef = pm.eci2ecef(*ECI, UTC)
1931

20-
rel = 0.025 if astropy is None else 0.0001
32+
rel = 0.025
2133

22-
assert ecef == approx([-5.7627e6, -1.6827e6, 3.1560e6], rel=rel)
34+
assert ecef == approx(ECEF, rel=rel)
35+
assert isinstance(ecef[0], float)
36+
assert isinstance(ecef[1], float)
37+
assert isinstance(ecef[2], float)
38+
39+
40+
def test_eci2ecef_astropy():
41+
pytest.importorskip("astropy")
42+
43+
ecef = pm.eci2ecef(*ECI, UTC)
44+
45+
rel = 0.0001
46+
47+
assert ecef == approx(ECEF, rel=rel)
48+
assert isinstance(ecef[0], float)
49+
assert isinstance(ecef[1], float)
50+
assert isinstance(ecef[2], float)
2351

2452

2553
def test_ecef2eci():
2654
pytest.importorskip("numpy")
2755
# this example from Matlab ecef2eci docs
28-
ecef = [-5762640, -1682738, 3156028]
29-
utc = datetime(2019, 1, 4, 12)
30-
eci = pm.ecef2eci(*ecef, utc)
56+
eci = pm.ecef2eci(*ECEF, UTC)
3157

3258
rel = 0.01 if astropy is None else 0.0001
3359

@@ -37,9 +63,7 @@ def test_ecef2eci():
3763
def test_eci2geodetic():
3864
pytest.importorskip("numpy")
3965

40-
eci = [-2981784, 5207055, 3161595]
41-
utc = datetime(2019, 1, 4, 12)
42-
lla = pm.eci2geodetic(*eci, utc)
66+
lla = pm.eci2geodetic(*ECI, UTC)
4367

4468
rel = 0.01 if astropy is None else 0.0001
4569

@@ -50,8 +74,8 @@ def test_geodetic2eci():
5074
pytest.importorskip("numpy")
5175

5276
lla = [27.880801, -163.722058, 408850.646]
53-
utc = datetime(2019, 1, 4, 12)
54-
eci = pm.geodetic2eci(*lla, utc)
77+
78+
eci = pm.geodetic2eci(*lla, UTC)
5579

5680
rel = 0.01 if astropy is None else 0.0001
5781

@@ -61,7 +85,7 @@ def test_geodetic2eci():
6185
def test_eci_aer():
6286
# test coords from Matlab eci2aer
6387
pytest.importorskip("numpy")
64-
t = datetime(2022, 1, 2, 3, 4, 5)
88+
t = datetime.datetime(2022, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
6589

6690
eci = [4500000, -45000000, 3000000]
6791
lla = [28, -80, 100]

0 commit comments

Comments
 (0)