Skip to content

Commit e1cbbb2

Browse files
committed
refactor attitude from framelib to sgp4lib
1 parent 621e5d4 commit e1cbbb2

5 files changed

Lines changed: 116 additions & 75 deletions

File tree

skyfield/framelib.py

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# -*- coding: utf-8 -*-
22
"""Raw transforms between coordinate frames, as NumPy matrices."""
33

4-
from numpy import array, cos, sin
4+
from numpy import array
55

66
from .constants import ANGVEL, ASEC2RAD, DAY_S, pi, tau
77
from .data.spice import inertial_frames as _inertial_frames
88
from .elementslib import osculating_elements_of
9-
from .functions import mxm, mxmxm, mxv, rot_x, rot_y, rot_z
9+
from .functions import mxm, mxmxm, rot_x, rot_y, rot_z
1010

1111

1212
def build_matrix():
@@ -155,15 +155,23 @@ def __repr__(self):
155155
def rotation_at(self, t):
156156
return self._matrix
157157

158-
class LVLH(InertialFrame):
158+
class LVLH(object):
159159
"""The Local Vertical Local Horizontal (LVLH) reference frame.
160160
161-
An inertial frame for a body in orbit, according to CCSDS conventions.
161+
A reference frame for a body in orbit, according to CCSDS conventions.
162162
The frame origin is centered on the body `position`, with z-axis in the
163163
direction of the `position.center` (i.e., the negative position vector),
164164
and y-axis opposite the orbital momentum vector.
165165
"""
166-
def __init__(self, position):
166+
def __init__(self, satellite):
167+
self.satellite = satellite
168+
self.__doc__ = (
169+
"Center ({0}) Pointing Local Vertical Local Horizontal"
170+
" reference frame."
171+
).format(satellite.center)
172+
173+
def rotation_at(self, t):
174+
position = self.satellite.at(t)
167175
elements = osculating_elements_of(position)
168176
i = elements.inclination.radians
169177
u = elements.argument_of_latitude.radians
@@ -172,34 +180,7 @@ def __init__(self, position):
172180
matrix = mxmxm(rot_x(pi / 2.0), rot_y(pi / 2.0), rot_y(om - pi))
173181
matrix = mxmxm(matrix, rot_z(pi - i), rot_y(-u))
174182

175-
doc = (
176-
"Center ({0}) Pointing Local Vertical Local Horizontal"
177-
" reference frame."
178-
).format(position.center)
179-
super(LVLH, self).__init__(doc, matrix)
180-
181-
def local_looking_vector(self, pitch, roll):
182-
"""Return a unit vector in the attitude direction in local coordinates.
183-
184-
Rotations should be provided as radians; `roll` is rotation about the
185-
x-axis and `pitch` is rotation about the y-axis.
186-
"""
187-
local_looking_vector = array(
188-
[sin(pitch), -sin(roll), cos(pitch) * cos(roll)]
189-
)
190-
191-
return local_looking_vector
192-
193-
def icrf_looking_vector(self, pitch, roll):
194-
"""Return a unit vector in the attitude direction, in ICRF coordinates.
195-
196-
Rotations should be provided in radians; `roll` is rotation about the
197-
x-axis and `pitch` is rotation about the y-axis.
198-
"""
199-
local_looking_vector = self.local_looking_vector(pitch, roll)
200-
icrf_looking_vector = mxv(self._matrix, local_looking_vector)
201-
202-
return icrf_looking_vector
183+
return matrix
203184

204185
equatorial_B1950_frame = InertialFrame(
205186
'Reference frame of the Earth’s mean equator and equinox at B1950.',

skyfield/sgp4lib.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from numpy import (
55
array, concatenate, identity, multiply, ones_like, repeat,
66
)
7+
from collections import namedtuple
78
from sgp4.api import SGP4_ERRORS, Satrec
89

910
from .constants import AU_KM, DAY_S, T0, tau
11+
from .framelib import LVLH
1012
from .functions import _T, mxm, mxv, rot_x, rot_y, rot_z
1113
from .searchlib import _find_discrete, find_maxima
1214
from .timelib import compute_calendar_date
@@ -281,6 +283,30 @@ def below_horizon_at(t):
281283
i = jd.argsort()
282284
return ts.tt_jd(jd[i]), v[i]
283285

286+
def _attitude(self, t, roll=0.0, pitch=0.0, yaw=0.0, rotation_order="xyz"):
287+
"""Return a unit vector in the attitude direction in local coordinates.
288+
289+
Rotations are applied to an initial vector pointing along the z-axis.
290+
Rotations should be provided as radians; `roll` is rotation about the
291+
x-axis, `pitch` is about the y-axis, and `yaw` is about the z-axis.
292+
"""
293+
rotations = {
294+
"x": rot_x(roll) if roll != 0 else _identity,
295+
"y": rot_y(pitch) if pitch != 0 else _identity,
296+
"z": rot_z(yaw) if yaw != 0 else _identity,
297+
}
298+
299+
lvlh_attitude = array([0, 0, 1])
300+
for axis in rotation_order:
301+
lvlh_attitude = mxv(rotations[axis], lvlh_attitude)
302+
303+
lvlh_frame = LVLH(self)
304+
direction = mxv(lvlh_frame.rotation_at(t), lvlh_attitude)
305+
Attitude = namedtuple("Attitude", "center, target, t")
306+
attitude = Attitude(self.at(t).position, direction, t)
307+
308+
return attitude
309+
284310
class TEME(object):
285311
"""The SGP4-specific True Equator Mean Equinox frame of reference.
286312

skyfield/tests/test_earth_satellites.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
22

3-
from numpy import array
3+
from numpy import allclose, array, arange, isclose
44
from skyfield import api
55
from skyfield.api import EarthSatellite, load
6-
from skyfield.constants import AU_KM, AU_M
6+
from skyfield.constants import AU_KM, AU_M, pi
7+
from skyfield.functions import angle_between, length_of
78
from skyfield.sgp4lib import TEME_to_ITRF, VectorFunction
89
from skyfield.timelib import julian_date
910

@@ -207,3 +208,41 @@ def _at(self, t):
207208
True, True, True, False,
208209
True, True, True, False, # Last two fake sats can see each other.
209210
]
211+
212+
def test_neutral_attitude():
213+
ts = api.load.timescale()
214+
times = [ts.utc(2023, 4, 8, 1 + i, 25) for i in range(10)]
215+
sat = EarthSatellite(line1, line2)
216+
217+
for t in times:
218+
a = EarthSatellite(line1, line2)._attitude(t)
219+
p = sat.at(t).position.au
220+
a = sat._attitude(t)
221+
# Attitude should be the negative of position vector.
222+
neg_p = -array(p) / length_of(array(p))
223+
assert allclose(a.center.au, p)
224+
assert allclose(a.target, neg_p)
225+
226+
def test_attitude():
227+
ts = api.load.timescale()
228+
t = ts.utc(2023, 4, 8, 1, 25)
229+
sat = EarthSatellite(line1, line2)
230+
pos = sat.at(t).position.au
231+
angles = array(arange(-pi, pi, pi / 8))
232+
233+
for angle in angles:
234+
abs_angle = abs(angle)
235+
a = sat._attitude(t, roll=angle)
236+
assert isclose(angle_between(a.target, -pos), abs_angle)
237+
238+
a = sat._attitude(t, pitch=angle)
239+
assert isclose(angle_between(a.target, -pos), abs_angle)
240+
241+
a = sat._attitude(t, yaw=angle)
242+
assert isclose(angle_between(a.target, -pos), 0.0)
243+
244+
a = sat._attitude(t, pitch=angle, yaw=angle)
245+
assert allclose(angle_between(a.target, -pos), abs_angle)
246+
247+
a = sat._attitude(t, roll=angle, yaw=angle)
248+
assert allclose(angle_between(a.target, -pos), abs_angle)

skyfield/tests/test_topos.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from assay import assert_raises
2+
from collections import namedtuple
23
from numpy import abs, arange, sqrt
34

45
from skyfield import constants
@@ -266,8 +267,10 @@ def test_deprecated_position_subpoint_method(ts, angle):
266267
def test_intersection_from_pole(ts):
267268
t = ts.utc(2018, 1, 19, 14, 37, 55)
268269
p = wgs84.latlon(90.0, 0.0, 1234.0).at(t)
269-
attitude = -p.xyz.au / length_of(p.xyz.au)
270-
earth_point = wgs84.intersection_of(p, attitude)
270+
direction = -p.xyz.au / length_of(p.xyz.au)
271+
Vector = namedtuple("Vector", "center, target, t")
272+
vector = Vector(p, direction, t)
273+
earth_point = wgs84.intersection_of(vector)
271274

272275
error_degrees = abs(earth_point.latitude.degrees - 90.0)
273276
error_mas = 60.0 * 60.0 * 1000.0 * error_degrees
@@ -277,8 +280,10 @@ def test_intersection_from_pole(ts):
277280
def test_intersection_from_equator(ts):
278281
t = ts.utc(2018, 1, 19, 14, 37, 55)
279282
p = wgs84.latlon(0.0, 0.0, 1234.0).at(t)
280-
attitude = -p.xyz.au / length_of(p.xyz.au)
281-
earth_point = wgs84.intersection_of(p, attitude)
283+
direction = -p.xyz.au / length_of(p.xyz.au)
284+
Vector = namedtuple("Vector", "center, target, t")
285+
vector = Vector(p, direction, t)
286+
earth_point = wgs84.intersection_of(vector)
282287

283288
error_degrees = abs(earth_point.latitude.degrees - 0.0)
284289
error_mas = 60.0 * 60.0 * 1000.0 * error_degrees
@@ -294,22 +299,26 @@ def test_limb_intersection_points(ts):
294299
d = 100.0
295300
a = wgs84.radius.au
296301
c = a * (1.0 - 1.0 / wgs84.inverse_flattening)
302+
pos = ICRF(position_au=[d, 0.0, 0.0], t=t, center=399)
297303

298-
# Attitude vectors pointing to the polar and equatorial limbs of the Earth
299-
attitude_bottom_tangent = [-d, 0.0, -c] / sqrt(d**2 + c**2)
300-
attitude_top_tangent = [-d, 0.0, c] / sqrt(d**2 + c**2)
301-
attitude_left_tangent = [-d, -a, 0.0] / sqrt(d**2 + c**2)
302-
attitude_right_tangent = [-d, a, 0.0] / sqrt(d**2 + c**2)
304+
# Vectors pointing to the polar and equatorial limbs of the Earth
305+
direction_bottom_tangent = [-d, 0.0, -c] / sqrt(d**2 + c**2)
306+
direction_top_tangent = [-d, 0.0, c] / sqrt(d**2 + c**2)
307+
direction_left_tangent = [-d, -a, 0.0] / sqrt(d**2 + c**2)
308+
direction_right_tangent = [-d, a, 0.0] / sqrt(d**2 + c**2)
309+
Vector = namedtuple("Vector", "center, target, t")
310+
bottom_tangent = Vector(pos, direction_bottom_tangent, t)
311+
top_tangent = Vector(pos, direction_top_tangent, t)
312+
left_tangent = Vector(pos, direction_left_tangent, t)
313+
right_tangent = Vector(pos, direction_right_tangent, t)
303314
# Attitude vector pointing straight down
304-
attitude_zenith = [-1.0, 0.0, 0.0]
305-
306-
pos = ICRF(position_au=[d, 0.0, 0.0], t=t, center=399)
315+
zenith = Vector(pos, [-1.0, 0.0, 0.0], t)
307316

308-
intersection_bottom = wgs84.intersection_of(pos, attitude_bottom_tangent)
309-
intersection_top = wgs84.intersection_of(pos, attitude_top_tangent)
310-
intersection_left = wgs84.intersection_of(pos, attitude_left_tangent)
311-
intersection_right = wgs84.intersection_of(pos, attitude_right_tangent)
312-
intersection_zenith = wgs84.intersection_of(pos, attitude_zenith)
317+
intersection_bottom = wgs84.intersection_of(bottom_tangent)
318+
intersection_top = wgs84.intersection_of(top_tangent)
319+
intersection_left = wgs84.intersection_of(left_tangent)
320+
intersection_right = wgs84.intersection_of(right_tangent)
321+
intersection_zenith = wgs84.intersection_of(zenith)
313322

314323
# Viewed from sufficient distance, points of intersection should be nearly
315324
# tangent to the north and south poles, and the zenith longitude +/- 90.0

skyfield/toposlib.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -277,32 +277,18 @@ def _compute_latitude(self, position):
277277

278278
subpoint = geographic_position_of # deprecated method name
279279

280-
def intersection_of(self, position, direction):
280+
def intersection_of(self, vector):
281281
"""Return the surface point intersected by a vector.
282282
283-
The ``position`` and ``direction`` describe the tail and orientation
284-
of the vector to intersect the ellipsoid, respectively. The vector must
285-
be provided in ICRF coordinates with a ``.center``` of 399, the center
286-
of the Earth. The direction should be an |xyz| unit vector. Returns a
287-
`GeographicPosition` giving the geodetic ``latitude`` and ``longitude``
288-
at the point that the ray intersects the surface of the Earth.
283+
TODO ...
284+
Returns a `GeographicPosition` giving the geodetic ``latitude`` and
285+
``longitude`` at the point that the ray intersects the surface of the
286+
Earth.
289287
290288
The main calculation implemented here is based on JPL's NAIF toolkit;
291289
https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/ellipses.html
292290
"""
293-
if not isinstance(position, ICRF):
294-
raise ValueError(
295-
'you can only calculate an intersection for a vector that is'
296-
' defined by coordinates along the ICRF axes'
297-
)
298-
if position.center != 399:
299-
raise ValueError(
300-
'you can only calculate an intersection for a vector that is'
301-
' defined relative to a position that is geocentric'
302-
' (center=399), but this position has a center of {0}'
303-
.format(position.center)
304-
)
305-
if position.t is None:
291+
if vector.t is None:
306292
raise ValueError(
307293
'you can only calculate an intersection for a vector that is'
308294
' defined at a fixed point in time (must have a non-null `.t`)'
@@ -317,9 +303,9 @@ def intersection_of(self, position, direction):
317303
inv_d_matrix = array([[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, c]])
318304

319305
# Apply distortion matrix to the vector
320-
position_xyz = position.xyz.au
306+
position_xyz = vector.center.xyz.au
321307
d_position_xyz = mxv(d_matrix, position_xyz)
322-
d_direction = mxv(d_matrix, direction)
308+
d_direction = mxv(d_matrix, vector.target)
323309
# Rescale the vector to unit length
324310
d_direction = d_direction / length_of(d_direction)
325311

@@ -341,7 +327,7 @@ def intersection_of(self, position, direction):
341327

342328
# Apply inverse distortion and convert back to geocentric coords
343329
intersection = Geocentric(
344-
mxv(inv_d_matrix, d_intersection), t=position.t
330+
mxv(inv_d_matrix, d_intersection), t=vector.t
345331
)
346332

347333
# Retrieve latitude and longitude

0 commit comments

Comments
 (0)