Skip to content

Commit d81a125

Browse files
committed
[#328] Attitude control action
1 parent 1f7e891 commit d81a125

File tree

5 files changed

+117
-29
lines changed

5 files changed

+117
-29
lines changed

docs/source/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Development - |version|
3636
By inheriting from a FSW class that overrides the ``MRPControlTask``, such as
3737
:class:`~bsk_rl.sim.fsw.BasicFSWModel` or :class:`~bsk_rl.sim.fsw.SteeringFSWModel`,
3838
users can implement custom attitude control strategies.
39+
* Add a continuous action for setting attitude in :class:`~bsk_rl.act.AttitudeSetpoint`.
3940

4041
Version 1.2.0
4142
-------------

src/bsk_rl/act/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ class MyActionSatellite(Satellite):
5454
+-----------------------------+-------------+-------------------------------------------------------------------------------------------------------+
5555
| :class:`ImpulsiveThrustHill`| 4 | Like :class:`ImpulsiveThrust`, but specified in the Hill frame of another satellite. |
5656
+-----------------------------+-------------+-------------------------------------------------------------------------------------------------------+
57-
57+
| :class:`AttitudeSetpoint` | 3 | Set the inertial attitude setpoint using Modified Rodrigues Parameters (MRPs). |
58+
+-----------------------------+-------------+-------------------------------------------------------------------------------------------------------+
5859
5960
6061
"""
6162

6263
from bsk_rl.act.actions import Action
6364
from bsk_rl.act.continuous_actions import (
65+
AttitudeSetpoint,
6466
ContinuousAction,
6567
ImpulsiveThrust,
6668
ImpulsiveThrustHill,
@@ -85,6 +87,7 @@ class MyActionSatellite(Satellite):
8587
"DiscreteFSWAction",
8688
"Charge",
8789
"Drift",
90+
"NadirPoint",
8891
"Desat",
8992
"Downlink",
9093
"Image",
@@ -93,4 +96,5 @@ class MyActionSatellite(Satellite):
9396
"ContinuousAction",
9497
"ImpulsiveThrust",
9598
"ImpulsiveThrustHill",
99+
"AttitudeSetpoint",
96100
]

src/bsk_rl/act/continuous_actions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,50 @@ def set_action(self, action: np.ndarray) -> None:
183183
dv_N = NH @ dv_H
184184

185185
super().set_action(np.concatenate((dv_N, [dt])))
186+
187+
188+
class AttitudeSetpoint(ContinuousAction):
189+
def __init__(
190+
self,
191+
name: str = "attitude_act",
192+
control_period: float = 60,
193+
) -> None:
194+
"""Set the attitude command to the specified MRP.
195+
196+
Args:
197+
name: Name of the action.
198+
control_period: Control period for the action. [s]
199+
"""
200+
super().__init__(name)
201+
self.control_period = control_period
202+
203+
@property
204+
def space(self) -> spaces.Box:
205+
"""Return the action space."""
206+
return spaces.Box(
207+
low=np.array([-1, -1, -1]),
208+
high=np.array([1, 1, 1]),
209+
shape=(3,),
210+
dtype=np.float32,
211+
)
212+
213+
@property
214+
def action_description(self) -> list[str]:
215+
"""Description of the continuous action space."""
216+
return ["mrp_1", "mrp_2", "mrp_3"]
217+
218+
def set_action(self, action: np.ndarray) -> None:
219+
"""Orient the satellite to the specified attitude.
220+
221+
Args:
222+
action: vector of [mrp_1, mrp_2, mrp_3]
223+
"""
224+
assert len(action) == 3, "Action must have 3 elements."
225+
226+
self.satellite.logger.info(
227+
f"Setting attitude command to {action} for {self.control_period} seconds."
228+
)
229+
self.satellite.fsw.action_attitude_mrp(action)
230+
self.satellite.update_timed_terminal_event(
231+
self.satellite.simulator.sim_time + self.control_period
232+
)

src/bsk_rl/sim/fsw/base.py

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,11 @@ def _zero_gateway_msgs(self) -> None:
247247
self.attGuidMsg.write(messaging.AttGuidMsgPayload())
248248

249249
def _make_task_list(self):
250-
return [self.MRPControlTask(self), self.NadirPointTask(self)]
250+
return [
251+
self.MRPControlTask(self),
252+
self.NadirPointTask(self),
253+
self.TrackingErrorTask(self),
254+
]
251255

252256
class MRPControlTask(Task):
253257
"""Task to control the satellite attitude magically (i.e. without actuators)."""
@@ -315,6 +319,38 @@ def action_nadir_point(self) -> None:
315319
BasicFSWModel.NadirPointTask.name + self.satellite.name
316320
)
317321

322+
class TrackingErrorTask(Task):
323+
"""Task to convert an attitude reference to guidance."""
324+
325+
name = "trackingErrTask"
326+
327+
def __init__(self, fsw, priority=90) -> None: # noqa: D107
328+
"""Task to convert an attitude reference to guidance."""
329+
super().__init__(fsw, priority)
330+
331+
def _create_module_data(self) -> None:
332+
self.trackingError = self.fsw.trackingError = (
333+
attTrackingError.attTrackingError()
334+
)
335+
self.trackingError.ModelTag = "trackingError"
336+
337+
def _setup_fsw_objects(self, **kwargs) -> None:
338+
self.trackingError.attNavInMsg.subscribeTo(
339+
self.fsw.dynamics.simpleNavObject.attOutMsg
340+
)
341+
self.trackingError.attRefInMsg.subscribeTo(self.fsw.attRefMsg)
342+
messaging.AttGuidMsg_C_addAuthor(
343+
self.trackingError.attGuidOutMsg, self.fsw.attGuidMsg
344+
)
345+
346+
self._add_model_to_task(self.trackingError, priority=1197)
347+
348+
@action
349+
def action_attitude_mrp(self, mrp: np.ndarray) -> None:
350+
"""Point the satellite to the specified attitude."""
351+
self.attRefMsg.write(messaging.AttRefMsgPayload(sigma_RN=mrp))
352+
self.simulator.enableTask(self.TrackingErrorTask.name + self.satellite.name)
353+
318354

319355
class BasicFSWModel(FSWModel):
320356
"""Basic FSW model with minimum necessary Basilisk components."""
@@ -327,7 +363,6 @@ def _make_task_list(self) -> list[Task]:
327363
return [
328364
self.SunPointTask(self),
329365
self.RWDesatTask(self),
330-
self.TrackingErrorTask(self),
331366
] + super()._make_task_list()
332367

333368
def _set_config_msgs(self) -> None:
@@ -538,32 +573,6 @@ def action_desat(self) -> None:
538573
raise ValueError(f"{self.desatAttitude} not a valid desatAttitude")
539574
self.simulator.enableTask(self.TrackingErrorTask.name + self.satellite.name)
540575

541-
class TrackingErrorTask(Task):
542-
"""Task to convert an attitude reference to guidance."""
543-
544-
name = "trackingErrTask"
545-
546-
def __init__(self, fsw, priority=90) -> None: # noqa: D107
547-
"""Task to convert an attitude reference to guidance."""
548-
super().__init__(fsw, priority)
549-
550-
def _create_module_data(self) -> None:
551-
self.trackingError = self.fsw.trackingError = (
552-
attTrackingError.attTrackingError()
553-
)
554-
self.trackingError.ModelTag = "trackingError"
555-
556-
def _setup_fsw_objects(self, **kwargs) -> None:
557-
self.trackingError.attNavInMsg.subscribeTo(
558-
self.fsw.dynamics.simpleNavObject.attOutMsg
559-
)
560-
self.trackingError.attRefInMsg.subscribeTo(self.fsw.attRefMsg)
561-
messaging.AttGuidMsg_C_addAuthor(
562-
self.trackingError.attGuidOutMsg, self.fsw.attGuidMsg
563-
)
564-
565-
self._add_model_to_task(self.trackingError, priority=1197)
566-
567576
class MRPControlTask(Task):
568577
"""Task to control the satellite attitude using reaction wheels."""
569578

tests/integration/act/test_int_actions_continuous.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import gymnasium as gym
22
import numpy as np
3+
import pytest
34
from pytest import approx
45

56
from bsk_rl import act, obs, sats
@@ -68,3 +69,29 @@ def test_thrust(self):
6869
self.env.step([0.0, 0.0, 0.0, 5.0])
6970
time_after = self.env.unwrapped.simulator.sim_time
7071
assert time_after - time_before == approx(5.0)
72+
73+
74+
class TestAttitudeSetpoint:
75+
@pytest.mark.parametrize("basic_types", [True, False])
76+
def test_attitude(self, basic_types):
77+
class AttitudeSat(sats.Satellite):
78+
if basic_types:
79+
dyn_type = dyn.BasicDynamicsModel
80+
fsw_type = fsw.BasicFSWModel
81+
else:
82+
dyn_type = dyn.DynamicsModel
83+
fsw_type = fsw.FSWModel
84+
observation_spec = [obs.SatProperties(dict(prop="sigma_BN"))]
85+
action_spec = [act.AttitudeSetpoint(control_period=360.0)]
86+
87+
env = gym.make(
88+
"SatelliteTasking-v1",
89+
satellite=AttitudeSat("AttitudeSat", sat_args=dict()),
90+
sim_rate=0.1,
91+
time_limit=3600,
92+
disable_env_checker=True,
93+
)
94+
95+
env.reset()
96+
observation, _, _, _, _ = env.step([0.1, 0.2, 0.3])
97+
assert np.allclose(observation, np.array([0.1, 0.2, 0.3]), atol=1e-4)

0 commit comments

Comments
 (0)