This tutorial walks through scenarioAttitudeFeedback.py, which demonstrates how to detumble a 6-DOF spacecraft in Earth orbit using a closed-loop MRP (Modified Rodrigues Parameters) attitude control law.
What this scenario demonstrates
- Setting up a 6-DOF spacecraft with mass and inertia properties
- Simulating an external disturbance torque via
extForceTorque
- Wiring navigation, guidance, error tracking, and control modules through messages
- Configuring the classic MRP feedback controller (
mrpFeedback)
- Optionally adding integral feedback to reject unknown constant disturbances
- Creating and writing
VehicleConfigMsg in Python
Modules and message flow
spacecraft → scStateOutMsg
↓
simpleNav.scStateInMsg
simpleNav → attOutMsg
↓
attTrackingError.attNavInMsg
inertial3D → attRefOutMsg
↓
attTrackingError.attRefInMsg
attTrackingError → attGuidOutMsg
↓
mrpFeedback.guidInMsg
VehicleConfigMsg → mrpFeedback.vehConfigInMsg
mrpFeedback → cmdTorqueOutMsg
↓
extForceTorque.cmdTorqueInMsg
Key modules
| Module | Package | Role |
|---|
spacecraft.Spacecraft | Basilisk.simulation | 6-DOF rigid body dynamics |
extForceTorque.ExtForceTorque | Basilisk.simulation | Applies control or disturbance torques |
simpleNav.SimpleNav | Basilisk.simulation | Passes through truth attitude and rate |
inertial3D.inertial3D | Basilisk.fswAlgorithms | Inertial reference frame definition |
attTrackingError.attTrackingError | Basilisk.fswAlgorithms | Computes attitude tracking error |
mrpFeedback.mrpFeedback | Basilisk.fswAlgorithms | Classic MRP feedback control law |
Running the scenario
python3 scenarioAttitudeFeedback.py
The run() function signature:
run(
True, # show_plots
False, # useUnmodeledTorque - adds a constant external disturbance
False, # useIntGain - enables integral feedback term
False, # useKnownTorque - feed-forwards the external torque
False, # useCMsg - uses C-wrapped message instead of C++
)
Step-by-step walkthrough
Create the simulation container
from Basilisk.utilities import SimulationBaseClass, macros
simTaskName = "simTask"
simProcessName = "simProcess"
scSim = SimulationBaseClass.SimBaseClass()
simulationTime = macros.min2nano(10.) # [ns] - 10-minute run
dynProcess = scSim.CreateNewProcess(simProcessName)
simulationTimeStep = macros.sec2nano(.1) # [ns] - 0.1-second step
dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep))
Configure spacecraft mass and inertia
Unlike the basic orbit scenario, attitude control requires realistic mass properties.from Basilisk.simulation import spacecraft
from Basilisk.utilities import unitTestSupport
scObject = spacecraft.Spacecraft()
scObject.ModelTag = "spacecraftBody"
I = [900., 0., 0.,
0., 800., 0.,
0., 0., 600.] # [kg*m^2] - inertia tensor about B, in B-frame
scObject.hub.mHub = 750.0 # [kg] - spacecraft mass
scObject.hub.r_BcB_B = [[0.0], [0.0], [0.0]] # [m] - CoM offset from B
scObject.hub.IHubPntBc_B = unitTestSupport.np2EigenMatrix3d(I)
scSim.AddModelToTask(simTaskName, scObject)
Add Earth gravity
from Basilisk.utilities import simIncludeGravBody
gravFactory = simIncludeGravBody.gravBodyFactory()
earth = gravFactory.createEarth()
earth.isCentralBody = True
mu = earth.mu
gravFactory.addBodiesTo(scObject)
Add the external force/torque effector
extForceTorque receives the control torque through its input message. You can also inject a constant disturbance torque directly on the module.from Basilisk.simulation import extForceTorque
extFTObject = extForceTorque.ExtForceTorque()
extFTObject.ModelTag = "externalDisturbance"
# Optionally inject an unmodeled disturbance torque [N*m]
if useUnmodeledTorque:
extFTObject.extTorquePntB_B = [[0.25], [-0.25], [0.1]]
scObject.addDynamicEffector(extFTObject)
scSim.AddModelToTask(simTaskName, extFTObject)
Set up the navigation and FSW modules
Wire together the navigation and flight software chain.from Basilisk.simulation import simpleNav
from Basilisk.fswAlgorithms import inertial3D, attTrackingError, mrpFeedback
# Navigation: passes spacecraft state truth as a navigation message
sNavObject = simpleNav.SimpleNav()
sNavObject.ModelTag = "SimpleNavigation"
scSim.AddModelToTask(simTaskName, sNavObject)
# Reference: inertial pointing at sigma_R0N = [0, 0, 0]
inertial3DObj = inertial3D.inertial3D()
inertial3DObj.ModelTag = "inertial3D"
inertial3DObj.sigma_R0N = [0., 0., 0.]
scSim.AddModelToTask(simTaskName, inertial3DObj)
# Tracking error: computes sigma_BR and omega_BR
attError = attTrackingError.attTrackingError()
attError.ModelTag = "attErrorInertial3D"
scSim.AddModelToTask(simTaskName, attError)
# MRP Feedback controller
mrpControl = mrpFeedback.mrpFeedback()
mrpControl.ModelTag = "mrpFeedback"
mrpControl.K = 3.5 # proportional gain
mrpControl.P = 30.0 # derivative gain
if useIntGain:
mrpControl.Ki = 0.0002 # integral gain
else:
mrpControl.Ki = -1 # negative value disables integral term
mrpControl.integralLimit = 2. / mrpControl.Ki * 0.1
if useKnownTorque:
mrpControl.knownTorquePntB_B = [0.25, -0.25, 0.1] # [N*m] feed-forward
scSim.AddModelToTask(simTaskName, mrpControl)
Create the vehicle configuration message
mrpFeedback requires the spacecraft inertia tensor to compute the control torque.from Basilisk.architecture import messaging
configData = messaging.VehicleConfigMsgPayload(ISCPntB_B=I)
configDataMsg = messaging.VehicleConfigMsg()
configDataMsg.write(configData)
Connect all messages
Each module’s input message subscribes to another module’s output message.sNavObject.scStateInMsg.subscribeTo(scObject.scStateOutMsg)
attError.attNavInMsg.subscribeTo(sNavObject.attOutMsg)
attError.attRefInMsg.subscribeTo(inertial3DObj.attRefOutMsg)
mrpControl.guidInMsg.subscribeTo(attError.attGuidOutMsg)
mrpControl.vehConfigInMsg.subscribeTo(configDataMsg)
extFTObject.cmdTorqueInMsg.subscribeTo(mrpControl.cmdTorqueOutMsg)
The subscribeTo() call establishes a zero-copy link between modules: no data is copied at connection time. The output message buffer is read directly by the subscribed module at each time step.
Set initial spacecraft state
Set both the orbital position and the initial attitude and angular rate.from Basilisk.utilities import orbitalMotion
oe = orbitalMotion.ClassicElements()
oe.a = 10000000.0 # [m] - 10,000 km semi-major axis
oe.e = 0.01
oe.i = 33.3 * macros.D2R # [rad]
oe.Omega = 48.2 * macros.D2R
oe.omega = 347.8 * macros.D2R
oe.f = 85.3 * macros.D2R
rN, vN = orbitalMotion.elem2rv(mu, oe)
scObject.hub.r_CN_NInit = rN # [m] - inertial position
scObject.hub.v_CN_NInit = vN # [m/s] - inertial velocity
scObject.hub.sigma_BNInit = [[0.1], [0.2], [-0.3]] # MRP initial attitude
scObject.hub.omega_BN_BInit = [[0.001], [-0.01], [0.03]] # [rad/s] initial rate
Set up data logging, initialize, and run
from Basilisk.utilities import unitTestSupport
numDataPoints = 100
samplingTime = unitTestSupport.samplingTime(
simulationTime, simulationTimeStep, numDataPoints
)
attErrorLog = attError.attGuidOutMsg.recorder(samplingTime)
mrpLog = mrpControl.cmdTorqueOutMsg.recorder(samplingTime)
snLog = sNavObject.scStateInMsg.recorder(samplingTime)
scSim.AddModelToTask(simTaskName, attErrorLog)
scSim.AddModelToTask(simTaskName, mrpLog)
scSim.AddModelToTask(simTaskName, snLog)
scSim.InitializeSimulation()
scSim.ConfigureStopTime(simulationTime)
scSim.ExecuteSimulation()
Retrieve results
import matplotlib.pyplot as plt
timeAxis = attErrorLog.times() # [ns]
# Attitude tracking error (MRP)
sigma_BR = attErrorLog.sigma_BR # shape (N, 3)
# Rate tracking error
omega_BR_B = attErrorLog.omega_BR_B # shape (N, 3) [rad/s]
# Commanded control torque
torque = mrpLog.torqueRequestBody # shape (N, 3) [N*m]
With useUnmodeledTorque=False, the MRP error converges to zero within a few minutes. Adding an integral gain (useIntGain=True) eliminates the steady-state offset caused by the disturbance.
Controller gain guide
| Parameter | Symbol | Effect |
|---|
K | proportional gain | Stiffness of the attitude response |
P | derivative gain | Damping — prevents overshoot |
Ki | integral gain | Eliminates steady-state error from constant torques; set negative to disable |
knownTorquePntB_B | feed-forward | Directly cancels a known constant torque |
Set mrpControl.Ki = -1 (negative) to disable the integral term without changing any other code paths. The implementation checks the sign of Ki internally.