Skip to content

Commit f0b9c2a

Browse files
committed
[#265] Add maximum range dynamics model
1 parent e8eb059 commit f0b9c2a

6 files changed

Lines changed: 143 additions & 4 deletions

File tree

docs/source/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Development - |version|
2929
* Allow for the ``time_limit`` to be randomized.
3030
* Added observation for arbitrary relative states between two satellites in :class:`~bsk_rl.obs.RelativeProperties`.
3131
* Allow for the ``transmitterPacketSize`` to be specified. The default sets it to the instrument's baud rate.
32+
* Add a maximum range checking dynamics model in :class:`~bsk_rl.dynamics.MaxRangeDynModel`. Useful for keeping an agent
33+
in the vicinity of a target early in training.
3234

3335

3436
Version 1.1.0

src/bsk_rl/sats/satellite.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,7 @@ def record_death(self, time: float) -> None:
282282
@property
283283
def _satellite_command(self) -> str:
284284
"""Generate string that refers to self in simBase."""
285-
return (
286-
"[satellite for satellite in self.satellites "
287-
+ f"if satellite.name=='{self.name}'][0]"
288-
)
285+
return f"self.get_satellite('{self.name}')"
289286

290287
def _info_command(self, info: str) -> str:
291288
"""Generate command to log to info from an event.

src/bsk_rl/sim/dyn.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,65 @@ def setup_conjunctions(self, conjunction_radius: float, **kwargs) -> None:
12561256
)
12571257

12581258

1259+
class MaxRangeDynModel(BasicDynamicsModel):
1260+
"""For evaluating a maximum range limitation between satellites."""
1261+
1262+
def __init__(self, *args, **kwargs) -> None:
1263+
"""Model that checks for maximum range violations between satellites.
1264+
1265+
The simulation is terminated at the time of separation and a range_valid failure is reported.
1266+
"""
1267+
super().__init__(*args, **kwargs)
1268+
self.out_of_ranges = []
1269+
1270+
def _setup_dynamics_objects(self, **kwargs) -> None:
1271+
super()._setup_dynamics_objects(**kwargs)
1272+
self.setup_range(**kwargs)
1273+
1274+
@aliveness_checker
1275+
def range_valid(self) -> bool:
1276+
"""Check if conjunction has not occurred."""
1277+
return len(self.out_of_ranges) == 0
1278+
1279+
@default_args(max_range_radius=5000, chief_name=None)
1280+
def setup_range(self, max_range_radius: float, chief_name: str, **kwargs) -> None:
1281+
"""Set up maximum distance checking relative to a chief satellite.
1282+
1283+
Args:
1284+
max_range_radius: [m] Maximum allowed range from the chief satellite.
1285+
chief_name: Chief satellite to check range against.
1286+
kwargs: Passed to other setup functions.
1287+
"""
1288+
self.max_range_radius = max_range_radius
1289+
self.chief_name = chief_name
1290+
1291+
if self.chief_name is None:
1292+
self.logger.warning(
1293+
"No chief satellite specified for maximum range checking. "
1294+
"Range checking is disabled."
1295+
)
1296+
return
1297+
1298+
self.simulator.createNewEvent(
1299+
valid_func_name(f"range_{self.satellite.name}_{self.chief_name}"),
1300+
macros.sec2nano(self.simulator.sim_rate),
1301+
True,
1302+
[
1303+
f"np.linalg.norm(np.array({self.satellite._satellite_command}.dynamics.r_BN_N)"
1304+
+ f"- np.array(self.get_satellite('{self.chief_name}').dynamics.r_BN_N))"
1305+
+ " >= "
1306+
+ f"{self.satellite._satellite_command}.dynamics.max_range_radius"
1307+
],
1308+
[
1309+
self.satellite._info_command(
1310+
f"Exceeded maximum range of {max_range_radius} m from {self.chief_name}"
1311+
),
1312+
f"{self.satellite._satellite_command}.dynamics.out_of_ranges.append(self.get_satellite('{self.chief_name}'))",
1313+
],
1314+
terminal=True,
1315+
)
1316+
1317+
12591318
__doc_title__ = "Dynamics Sims"
12601319
__all__ = [
12611320
"DynamicsModel",
@@ -1265,5 +1324,6 @@ def setup_conjunctions(self, conjunction_radius: float, **kwargs) -> None:
12651324
"ContinuousImagingDynModel",
12661325
"GroundStationDynModel",
12671326
"ConjunctionDynModel",
1327+
"MaxRangeDynModel",
12681328
"FullFeaturedDynModel",
12691329
]

src/bsk_rl/sim/simulator.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ def delete_event(self, event_name) -> None:
162162
self.eventList.remove(event)
163163
del self.eventMap[event_name]
164164

165+
def get_satellite(self, name: str) -> "Satellite":
166+
"""Get a satellite by name.
167+
168+
Args:
169+
name: Name of the satellite to retrieve.
170+
171+
Returns:
172+
The satellite object with the specified name.
173+
"""
174+
for sat in self.satellites:
175+
if sat.name == name:
176+
return sat
177+
raise ValueError(f"Satellite with name '{name}' not found.")
178+
165179
def __del__(self):
166180
"""Log when simulator is deleted."""
167181
logger.debug("Basilisk simulator deleted")

tests/integration/sim/test_int_dynamics.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,66 @@ class CollisionSat(sats.Satellite):
158158
assert sat1.dynamics.conjunctions == []
159159
assert sat2.dynamics.conjunctions == []
160160
assert sat3.dynamics.conjunctions == []
161+
162+
163+
class TestMaxRangeDynModel:
164+
@pytest.mark.parametrize(
165+
"rN1,rN2,max_range_violation",
166+
[
167+
([1e8, 0, 0], [1e8 + 10, 0, 0], False),
168+
([1e8, 0, 0], [1e8 + 200, 0, 0], True),
169+
([0, 1e8, 0], [1e8, 0, 0], True),
170+
],
171+
)
172+
def test_max_range(self, rN1, rN2, max_range_violation):
173+
class ChiefSat(sats.Satellite):
174+
fsw_type = fsw.BasicFSWModel
175+
dyn_type = dyn.BasicDynamicsModel
176+
observation_spec = [obs.Time()]
177+
action_spec = [act.Drift()]
178+
179+
class DeputySat(sats.Satellite):
180+
fsw_type = fsw.BasicFSWModel
181+
dyn_type = dyn.MaxRangeDynModel
182+
observation_spec = [obs.Time()]
183+
action_spec = [act.Drift()]
184+
185+
env = gym.make(
186+
"ConstellationTasking-v1",
187+
satellites=[
188+
ChiefSat(
189+
"Chief",
190+
sat_args=dict(
191+
rN=rN1,
192+
vN=[0, 0, 0],
193+
oe=None,
194+
),
195+
),
196+
DeputySat(
197+
"Deputy",
198+
sat_args=dict(
199+
rN=rN2,
200+
vN=[0, 0, 0],
201+
oe=None,
202+
chief_name="Chief",
203+
max_range_radius=100,
204+
),
205+
),
206+
],
207+
sim_rate=1.0,
208+
time_limit=100.0,
209+
max_step_duration=1e9,
210+
disable_env_checker=True,
211+
)
212+
213+
env.reset()
214+
215+
env.step(dict(Chief=0, Deputy=0))
216+
217+
sat1 = env.unwrapped.satellites[0]
218+
sat2 = env.unwrapped.satellites[1]
219+
220+
if max_range_violation:
221+
assert sat2.dynamics.out_of_ranges == [sat1]
222+
else:
223+
assert sat2.dynamics.out_of_ranges == []

tests/unittest/sats/test_satellite.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from functools import partial
12
from unittest.mock import MagicMock, patch
23

34
import numpy as np
45
import pytest
56

67
from bsk_rl import sats
8+
from bsk_rl.sim import Simulator
79
from bsk_rl.sim.fsw import Task
810

911

@@ -110,6 +112,7 @@ def test_satellite_command(self):
110112
sat1 = sats.Satellite(name="TestSat_1", sat_args={})
111113
sat2 = sats.Satellite(name="TestSat_2", sat_args={})
112114
self.satellites = [sat1, sat2]
115+
self.get_satellite = partial(Simulator.get_satellite, self)
113116
assert sat1 == eval(sat1._satellite_command)
114117
assert sat1 != eval(sat2._satellite_command)
115118
assert sat2 == eval(sat2._satellite_command)

0 commit comments

Comments
 (0)