Skip to content

Commit e127763

Browse files
committed
[#266] Orbital element observations
1 parent 7ba099b commit e127763

4 files changed

Lines changed: 119 additions & 1 deletion

File tree

docs/source/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Development - |version|
3131
* Allow for the ``transmitterPacketSize`` to be specified. The default sets it to the instrument's baud rate.
3232
* Add a maximum range checking dynamics model in :class:`~bsk_rl.dynamics.MaxRangeDynModel`. Useful for keeping an agent
3333
in the vicinity of a target early in training.
34+
* Add properties in spacecraft dynamics for orbital element observations.
3435

3536

3637
Version 1.1.0

src/bsk_rl/sim/dyn/base.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,69 @@ def omega_BH_H(self):
211211
HN = rv2HN(self.r_BN_N, self.v_BN_N)
212212
return HN @ omega_BH_N
213213

214+
def _compute_oes(self):
215+
if (
216+
not hasattr(self, "_oe_cache_time")
217+
or self._oe_cache_time != self.simulator.time
218+
):
219+
self._oe_cache = orbitalMotion.rv2elem(
220+
mu=self.mu, rVec=np.array(self.r_BN_N), vVec=np.array(self.v_BN_N)
221+
)
222+
self._oe_cache_time = getattr(self.simulator, "time", None)
223+
return self._oe_cache
224+
225+
@property
226+
def semi_major_axis(self):
227+
"""Semimajor axis of the satellite's orbit [km]."""
228+
return self._compute_oes().a
229+
230+
@property
231+
def eccentricity(self):
232+
"""Eccentricity of the satellite's orbit [-]."""
233+
return self._compute_oes().e
234+
235+
@property
236+
def inclination(self):
237+
"""Inclination of the satellite's orbit [rad]."""
238+
return self._compute_oes().i
239+
240+
@property
241+
def ascending_node(self):
242+
"""Longitude of ascending node of the satellite's orbit [rad]."""
243+
return self._compute_oes().AN
244+
245+
@property
246+
def argument_of_periapsis(self):
247+
"""Argument of periapsis of the satellite's orbit [rad]."""
248+
return self._compute_oes().AP
249+
250+
@property
251+
def true_anomaly(self):
252+
"""True anomaly of the satellite's orbit [rad]."""
253+
return self._compute_oes().f
254+
255+
@property
256+
def beta_angle(self):
257+
"""Beta angle of the satellite's orbit, between 0 and 2pi [rad].
258+
259+
The angle between the angular momentum vector and the sun direction vector.
260+
"""
261+
r_BN_N = self.dynamics.r_BN_N
262+
v_BN_N = self.dynamics.v_BN_N
263+
h_N = np.cross(r_BN_N, v_BN_N)
264+
r_SN_N = (
265+
self.simulator.world.gravFactory.spiceObject.planetStateOutMsgs[
266+
self.simulator.world.sun_index
267+
]
268+
.read()
269+
.PositionVector
270+
)
271+
272+
beta = np.arccos(
273+
np.dot(h_N, r_SN_N) / (np.linalg.norm(h_N) * np.linalg.norm(r_SN_N))
274+
)
275+
return beta
276+
214277
@property
215278
def battery_charge(self):
216279
"""Battery charge [W*s]."""
@@ -293,6 +356,7 @@ def setup_spacecraft_hub(
293356
priority: Model priority.
294357
kwargs: Passed to other setup functions.
295358
"""
359+
self.mu = mu
296360
if rN is not None and vN is not None and oe is None:
297361
pass
298362
elif oe is not None and rN is None and vN is None:

tests/integration/obs/test_int_observations.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class SatPropertiesSat(sats.Satellite):
6262
obs.SatProperties(
6363
dict(prop="r_BN_N", module="dynamics"),
6464
dict(prop="r_BN_N", norm=7000.0 * 1e3),
65+
dict(prop="inclination", module="dynamics", norm=np.pi / 180),
6566
),
6667
]
6768
action_spec = [act.Drift()]
@@ -71,7 +72,7 @@ class SatPropertiesSat(sats.Satellite):
7172
satellite=SatPropertiesSat(
7273
"Sputnik",
7374
sat_args=SatPropertiesSat.default_sat_args(
74-
oe=random_orbit(r_body=7000, alt=0)
75+
oe=random_orbit(r_body=7000, alt=0, i=45)
7576
),
7677
),
7778
scenario=UniformTargets(n_targets=0),
@@ -86,6 +87,7 @@ def test_normd_property_state(self):
8687
observation, info = self.env.reset()
8788
assert np.linalg.norm(observation[0:3]) == approx(7000.0 * 1e3)
8889
assert np.linalg.norm(observation[3:6]) == approx(1.0)
90+
assert observation[6] == approx(45) # Inclination in radians
8991

9092

9193
class TestTime:

tests/unittest/sim/test_dynamics.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ImagingDynModel,
1414
LOSCommDynModel,
1515
)
16+
from bsk_rl.sim.dyn import base as dyn_module
1617

1718
module = "bsk_rl.sim.dyn."
1819

@@ -79,6 +80,56 @@ def test_dynamic_properties(self):
7980
assert (dyn.v_BN_P == np.zeros(3)).all()
8081
assert (dyn.omega_BP_P == np.zeros(3)).all()
8182

83+
def test_orbit_properties(self):
84+
with patch.object(dyn_module, "orbitalMotion") as mocked:
85+
mocked.rv2elem = MagicMock(
86+
return_value=MagicMock(a=1.0, e=2.0, i=3.0, AN=4.0, AP=5.0, f=6.0)
87+
)
88+
dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0)
89+
dyn.mu = 1
90+
dyn.scObject = MagicMock()
91+
dyn.scObject.scStateOutMsg.read.return_value = MagicMock(
92+
r_BN_N=[1, 2, 3], v_BN_N=[4, 5, 6]
93+
)
94+
dyn.simulator.time = 0.0
95+
96+
# Check that call to rv2elem happens only once
97+
assert dyn.semi_major_axis == 1.0
98+
mocked.rv2elem.assert_called_once()
99+
assert dyn.eccentricity == 2.0
100+
assert dyn.inclination == 3.0
101+
assert dyn.ascending_node == 4.0
102+
assert dyn.argument_of_periapsis == 5.0
103+
assert dyn.true_anomaly == 6.0
104+
mocked.rv2elem.assert_called_once()
105+
106+
def test_orbit_properties_no_cache_update(self):
107+
with patch.object(dyn_module, "orbitalMotion") as mocked:
108+
mocked.rv2elem = MagicMock(return_value=MagicMock(a=1.0))
109+
dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0)
110+
dyn.mu = 1
111+
dyn.scObject = MagicMock()
112+
dyn.scObject.scStateOutMsg.read.return_value = MagicMock()
113+
dyn.simulator.time = 0.0
114+
115+
assert dyn.semi_major_axis == 1.0
116+
mocked.rv2elem = MagicMock(return_value=MagicMock(a=2.0))
117+
assert dyn.semi_major_axis == 1.0
118+
119+
def test_orbit_properties_cache_update(self):
120+
with patch.object(dyn_module, "orbitalMotion") as mocked:
121+
mocked.rv2elem = MagicMock(return_value=MagicMock(a=1.0))
122+
dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0)
123+
dyn.mu = 1
124+
dyn.scObject = MagicMock()
125+
dyn.scObject.scStateOutMsg.read.return_value = MagicMock()
126+
dyn.simulator.time = 0.0
127+
128+
assert dyn.semi_major_axis == 1.0
129+
dyn.simulator.time = 1.0
130+
mocked.rv2elem = MagicMock(return_value=MagicMock(a=2.0))
131+
assert dyn.semi_major_axis == 2.0
132+
82133
def test_battery_properties(self):
83134
dyn = BasicDynamicsModel(MagicMock(simulator=MagicMock()), 1.0)
84135
dyn.powerMonitor = MagicMock()

0 commit comments

Comments
 (0)