#
# ISC License
#
# Copyright (c) 2022, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
r"""
Overview
--------
This script shows how to perform momentum dumping when the momentum accumulated on the reaction wheels
is above a user-defined threshold. In this case, such threshold is set at 80 Nms. The dumping is performed
by a set of 8 thrusters that can provide control about the three principal axes of the spacecraft.
To perform the momentum dumping, three concatenated modules are used:
- :ref:`thrMomentumManagement`: computes the amount of momentum to be dumped, based on current stored momentum
and the user-defined threshold. It is important to notice that, for the three concatenated modules to work
correctly, this first module cannot be run at simulation time :math:`t = 0`. In this script, the method
``Reset`` is called on :ref:`thrMomentumManagement` at :math:`t = 10` s, which coincides to the time at which
the first desaturating impulse is fired.
- :ref:`thrForceMapping`: maps the amout of momentum to be dumped into impulses that must be delivered by each
thruster. This module is originally implemented to map a requested torque into forces imparted by the thrusters,
but it can be applied in this case as well, because the math is the same. The only caveat is that, in this case,
the output should not be scaled by the thruster maximum torque capability, since the desired output is an impulse
and not a torque. To deactivate the output scaling, the ``angErrThresh`` input variable for this module must be
set to a value larger than :math:`\pi`, as specified in the module documentation.
- :ref:`thrMomentumDumping`: computes the thruster on-times required to deliver the desired impulse. A
``maxCounterValue`` of 100 is used in this example to allow the spacecraft to maneuver back to the desired attitude
after each time the thrusters fire.
For this script to work as intended, it is necessary to run the flight software and the dynamics at two different
frequencies. In this example, the simulation time step for the flight software is 1 second, whereas for the dynamics
it is 0.1 seconds. This is necessary because the :ref:`thrMomentumDumping` automatically uses the task time step as
control period for the firing. However, if the dynamics is integrated at the same frequency, this does not give
enough time resolution to appreciate the variation in the momentum.
The script is found in the folder ``basilisk/examples`` and executed by using::
python3 scenarioMomentumDumping.py
Illustration of Simulation Results
----------------------------------
In this examples, the spacecraft is already at the desired attitude, but the four reaction wheels are saturated (the total
angular momentum exceeds the threshold). The desaturation happens at :math:`t = 10` when the :ref:`thrMomentumManagement` is
reset. Three firings are sufficient to dump the momentum below the set threshold. The following figures illustrate the change
in momentum for the four wheels :math:`H_i` for :math:`i = 1,...,4` and the total angular momentum :math:`\|H\|`, and the
attitude errors, as functions of time, with respect to the desired target attitude.
.. image:: /_images/Scenarios/scenarioMomentumDumping3.svg
:align: center
.. image:: /_images/Scenarios/scenarioMomentumDumping1.svg
:align: center
The plots show that the momentum is dumped below the threshold. Also, the desired attitude is recovered between the first and
second firing, and after the third, but between the second and the third there is not enough time for the spacecraft to slew
back to that attitude.
The next two plots show the amount of impulse [Ns] requested for each thruster, and the times during which each thruster is
operational. As expected, 100 control times pass between each firing: because the control time coincides with the flight
software simulation time step of 1 s, this means that firings are 100 seconds apart.
.. image:: /_images/Scenarios/scenarioMomentumDumping5.svg
:align: center
.. image:: /_images/Scenarios/scenarioMomentumDumping7.svg
:align: center
"""
import os
import matplotlib.pyplot as plt
import numpy as np
from Basilisk import __path__
from Basilisk.architecture import messaging
from Basilisk.fswAlgorithms import (mrpFeedback, attTrackingError, inertial3D, rwMotorTorque,
thrMomentumManagement, thrForceMapping, thrMomentumDumping)
from Basilisk.simulation import (reactionWheelStateEffector, thrusterDynamicEffector, simpleNav, spacecraft)
from Basilisk.utilities import (SimulationBaseClass, macros,
orbitalMotion, simIncludeGravBody,
simIncludeRW, simIncludeThruster, unitTestSupport, vizSupport)
bskPath = __path__[0]
fileName = os.path.basename(os.path.splitext(__file__)[0])
def run(show_plots):
# Create simulation variable names
fswTask = "fswTask"
dynTask = "dynTask"
simProcessName = "simProcess"
# Create a sim module as an empty container
scSim = SimulationBaseClass.SimBaseClass()
scSim.SetProgressBar(False)
# create the simulation process
dynProcess = scSim.CreateNewProcess(simProcessName)
# create the dynamics task and specify the simulation time and integration update time
simulationTime = macros.min2nano(5)
simulationTimeStepFsw = macros.sec2nano(1)
simulationTimeStepDyn = macros.sec2nano(0.1)
dynProcess.addTask(scSim.CreateNewTask(dynTask, simulationTimeStepDyn))
dynProcess.addTask(scSim.CreateNewTask(fswTask, simulationTimeStepFsw))
#
# setup the simulation tasks/objects
#
# initialize spacecraft object and set properties
scObject = spacecraft.Spacecraft()
scObject.ModelTag = "Max-SC"
# add spacecraft object to the simulation process
scSim.AddModelToTask(dynTask, scObject, 1)
# setup Gravity Body
gravFactory = simIncludeGravBody.gravBodyFactory()
# Next a series of gravitational bodies are included
gravBodies = gravFactory.createBodies('earth', 'sun')
planet = gravBodies['earth']
planet.isCentralBody = True
mu = planet.mu
# The configured gravitational bodies are added to the spacecraft dynamics with the usual command:
gravFactory.addBodiesTo(scObject)
# Next, the default SPICE support module is created and configured.
timeInitString = "2022 JUNE 27 00:00:00.0"
# The following is a support macro that creates a `spiceObject` instance
spiceObject = gravFactory.createSpiceInterface(time=timeInitString, epochInMsg=True)
# Earth is gravity center
spiceObject.zeroBase = 'Earth'
# The SPICE object is added to the simulation task list.
scSim.AddModelToTask(fswTask, spiceObject, 2)
# The gravitational body is connected to the spacecraft object
gravFactory.addBodiesTo(scObject)
# setup the orbit using classical orbit elements
oe = orbitalMotion.ClassicElements()
oe.a = 7000. * 1000 # meters
oe.e = 0.0001
oe.i = 33.3 * macros.D2R
oe.Omega = 148.2 * macros.D2R
oe.omega = 347.8 * macros.D2R
oe.f = 335 * macros.D2R
rN, vN = orbitalMotion.elem2rv(mu, oe)
# To set the spacecraft initial conditions, the following initial position and velocity variables are set:
scObject.hub.r_CN_NInit = rN # m - r_BN_N
scObject.hub.v_CN_NInit = vN # m/s - v_BN_N
scObject.hub.sigma_BNInit = [0, 0., 0.] # MRP set to customize initial inertial attitude
scObject.hub.omega_BN_BInit = [[0.], [0.], [0.]] # rad/s - omega_CN_B
# define the simulation inertia
I = [1700, 0., 0.,
0., 1700, 0.,
0., 0., 1800]
scObject.hub.mHub = 2500 # kg - spacecraft mass
scObject.hub.r_BcB_B = [[0.0], [0.0], [1.28]] # m - position vector of body-fixed point B relative to CM
scObject.hub.IHubPntBc_B = unitTestSupport.np2EigenMatrix3d(I)
#
# add RW devices
#
# Make RW factory instance
rwFactory = simIncludeRW.rwFactory()
# store the RW dynamical model type
varRWModel = messaging.BalancedWheels
# create each RW by specifying the RW type, the spin axis gsHat, plus optional arguments
c = 2**(-0.5)
RW1 = rwFactory.create('Honeywell_HR16', [c, 0, c], maxMomentum=100., Omega=4000. # RPM
, RWModel=varRWModel)
RW2 = rwFactory.create('Honeywell_HR16', [0, c, c], maxMomentum=100., Omega=2000. # RPM
, RWModel=varRWModel)
RW3 = rwFactory.create('Honeywell_HR16', [-c, 0, c], maxMomentum=100., Omega=3500. # RPM
, RWModel=varRWModel)
RW4 = rwFactory.create('Honeywell_HR16', [0, -c, c], maxMomentum=100., Omega=0. # RPM
, RWModel=varRWModel)
numRW = rwFactory.getNumOfDevices()
RW = [RW1, RW2, RW3, RW4]
# create RW object container and tie to spacecraft object
rwStateEffector = reactionWheelStateEffector.ReactionWheelStateEffector()
rwStateEffector.ModelTag = "RW_cluster"
rwFactory.addToSpacecraft(scObject.ModelTag, rwStateEffector, scObject)
# add RW object array to the simulation process
scSim.AddModelToTask(dynTask, rwStateEffector, 2)
# Setup the FSW RW configuration message.
fswRwParamMsg = rwFactory.getConfigMessage()
# create arrays for thrusters' locations and directions
a = 1
b = 1.28
location = [
[-a, -a, b],
[ a, -a, -b],
[ a, -a, b],
[ a, a, -b],
[ a, a, b],
[-a, a, -b],
[-a, a, b],
[-a, -a, -b]
]
direction = [
[ 1, 0, 0],
[-1, 0, 0],
[ 0, 1, 0],
[ 0, -1, 0],
[-1, 0, 0],
[ 1, 0, 0],
[ 0, -1, 0],
[ 0, 1, 0]
]
# create the set of thruster in the dynamics task
thrusterSet = thrusterDynamicEffector.ThrusterDynamicEffector()
scSim.AddModelToTask(dynTask, thrusterSet)
# Make a fresh thruster factory instance, this is critical to run multiple times
thFactory = simIncludeThruster.thrusterFactory()
# create the thruster devices by specifying the thruster type and its location and direction
for pos_B, dir_B in zip(location, direction):
thFactory.create('MOOG_Monarc_5', pos_B, dir_B, MaxThrust=5.0)
# get number of thruster devices
numTh = thFactory.getNumOfDevices()
# create thruster object container and tie to spacecraft object
thrModelTag = "ACSThrusterDynamics"
thFactory.addToSpacecraft(thrModelTag, thrusterSet, scObject)
# add the simple Navigation sensor module
sNavObject = simpleNav.SimpleNav()
sNavObject.ModelTag = "SimpleNavigation"
scSim.AddModelToTask(dynTask, sNavObject)
# Setup the FSW thruster configuration message.
fswThrConfigMsg = thFactory.getConfigMessage()
#
# setup the FSW algorithm tasks
#
# setup inertial3D guidance module
inertial3DObj = inertial3D.inertial3D()
inertial3DObj.ModelTag = "inertial3D"
scSim.AddModelToTask(fswTask, inertial3DObj)
inertial3DObj.sigma_R0N = [0., 0., 0.] # set the desired inertial orientation
# setup the attitude tracking error evaluation module
attError = attTrackingError.attTrackingError()
attError.ModelTag = "attErrorInertial3D"
scSim.AddModelToTask(fswTask, attError)
# setup the MRP Feedback control module
mrpControl = mrpFeedback.mrpFeedback()
mrpControl.ModelTag = "mrpFeedback"
scSim.AddModelToTask(fswTask, mrpControl)
decayTime = 10.0
xi = 1.0
mrpControl.Ki = -1 # make value negative to turn off integral feedback
mrpControl.P = 3*np.max(I)/decayTime
mrpControl.K = (mrpControl.P/xi)*(mrpControl.P/xi)/np.max(I)
mrpControl.integralLimit = 2. / mrpControl.Ki * 0.1
controlAxes_B = [1, 0, 0, 0, 1, 0, 0, 0, 1]
# add module that maps the Lr control torque into the RW motor torques
rwMotorTorqueObj = rwMotorTorque.rwMotorTorque()
rwMotorTorqueObj.ModelTag = "rwMotorTorque"
scSim.AddModelToTask(fswTask, rwMotorTorqueObj)
# Make the RW control all three body axes
rwMotorTorqueObj.controlAxes_B = controlAxes_B
# Momentum dumping configuration
thrDesatControl = thrMomentumManagement.thrMomentumManagement()
thrDesatControl.ModelTag = "thrMomentumManagement"
scSim.AddModelToTask(fswTask, thrDesatControl)
thrDesatControl.hs_min = 80 # Nms : maximum wheel momentum
# setup the thruster force mapping module
thrForceMappingObj = thrForceMapping.thrForceMapping()
thrForceMappingObj.ModelTag = "thrForceMapping"
scSim.AddModelToTask(fswTask, thrForceMappingObj)
thrForceMappingObj.controlAxes_B = controlAxes_B
thrForceMappingObj.thrForceSign = 1
thrForceMappingObj.angErrThresh = 3.15 # this needs to be larger than pi (180 deg) for the module to work in the momentum dumping scenario
# setup the thruster momentum dumping module
thrDump = thrMomentumDumping.thrMomentumDumping()
thrDump.ModelTag = "thrDump"
scSim.AddModelToTask(fswTask, thrDump)
thrDump.maxCounterValue = 100 # number of control periods (simulationTimeStepFsw) to wait between two subsequent on-times
thrDump.thrMinFireTime = 0.02 # thruster firing resolution
#
# create simulation messages
#
# create the FSW vehicle configuration message
vehicleConfigOut = messaging.VehicleConfigMsgPayload()
vehicleConfigOut.ISCPntB_B = I # use the same inertia in the FSW algorithm as in the simulation
vcMsg = messaging.VehicleConfigMsg().write(vehicleConfigOut)
# if this scenario is to interface with the BSK Viz, uncomment the following lines
viz = vizSupport.enableUnityVisualization(scSim, dynTask, scObject
# , saveFile=fileName
, rwEffectorList=rwStateEffector
, thrEffectorList=thrusterSet
)
vizSupport.setActuatorGuiSetting(viz, viewRWPanel=True,
viewRWHUD=True,
viewThrusterPanel=True,
viewThrusterHUD=True
)
# link messages
attError.attNavInMsg.subscribeTo(sNavObject.attOutMsg)
sNavObject.scStateInMsg.subscribeTo(scObject.scStateOutMsg)
attError.attNavInMsg.subscribeTo(sNavObject.attOutMsg)
attError.attRefInMsg.subscribeTo(inertial3DObj.attRefOutMsg)
mrpControl.guidInMsg.subscribeTo(attError.attGuidOutMsg)
mrpControl.vehConfigInMsg.subscribeTo(vcMsg)
mrpControl.rwParamsInMsg.subscribeTo(fswRwParamMsg)
mrpControl.rwSpeedsInMsg.subscribeTo(rwStateEffector.rwSpeedOutMsg)
rwMotorTorqueObj.rwParamsInMsg.subscribeTo(fswRwParamMsg)
rwMotorTorqueObj.vehControlInMsg.subscribeTo(mrpControl.cmdTorqueOutMsg)
rwStateEffector.rwMotorCmdInMsg.subscribeTo(rwMotorTorqueObj.rwMotorTorqueOutMsg)
thrDesatControl.rwSpeedsInMsg.subscribeTo(rwStateEffector.rwSpeedOutMsg)
thrDesatControl.rwConfigDataInMsg.subscribeTo(fswRwParamMsg)
thrForceMappingObj.cmdTorqueInMsg.subscribeTo(thrDesatControl.deltaHOutMsg)
thrForceMappingObj.thrConfigInMsg.subscribeTo(fswThrConfigMsg)
thrForceMappingObj.vehConfigInMsg.subscribeTo(vcMsg)
thrDump.thrusterConfInMsg.subscribeTo(fswThrConfigMsg)
thrDump.deltaHInMsg.subscribeTo(thrDesatControl.deltaHOutMsg)
thrDump.thrusterImpulseInMsg.subscribeTo(thrForceMappingObj.thrForceCmdOutMsg)
thrusterSet.cmdsInMsg.subscribeTo(thrDump.thrusterOnTimeOutMsg)
#
# Setup data logging before the simulation is initialized
#
numDataPoints = 5000
samplingTime = unitTestSupport.samplingTime(simulationTime, simulationTimeStepDyn, numDataPoints)
sNavRec = sNavObject.attOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, sNavRec)
dataRec = scObject.scStateOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, dataRec)
rwMotorLog = rwMotorTorqueObj.rwMotorTorqueOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, rwMotorLog)
attErrorLog = attError.attGuidOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, attErrorLog)
deltaHLog = thrDesatControl.deltaHOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, deltaHLog)
thrMapLog = thrForceMappingObj.thrForceCmdOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, thrMapLog)
onTimeLog = thrDump.thrusterOnTimeOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, onTimeLog)
# thrLog = fswThrConfigMsg.recorder(samplingTime)
# scSim.AddModelToTask(simTaskName, thrLog)
# To log the RW information, the following code is used:
mrpLog = rwStateEffector.rwSpeedOutMsg.recorder(samplingTime)
scSim.AddModelToTask(dynTask, mrpLog)
# A message is created that stores an array of the Omega wheel speeds
rwLogs = []
for item in range(numRW):
rwLogs.append(rwStateEffector.rwOutMsgs[item].recorder(samplingTime))
scSim.AddModelToTask(dynTask, rwLogs[item])
# To log the thruster information, the following code is used:
thrLog = []
for item in range(numTh):
thrLog.append(thrusterSet.thrusterOutMsgs[item].recorder(samplingTime))
scSim.AddModelToTask(dynTask, thrLog[item])
# initialize Simulation: This function runs the self_init()
# cross_init() and reset() routines on each module.
scSim.InitializeSimulation()
# configure a simulation stop time and execute the simulation run
scSim.ConfigureStopTime(macros.sec2nano(10.0))
scSim.ExecuteSimulation()
# reset thrDesat module after 10 seconds because momentum cannot be dumped at t = 0
thrDesatControl.Reset(macros.sec2nano(10.0))
scSim.ConfigureStopTime(simulationTime)
scSim.ExecuteSimulation()
# retrieve the logged data
# RW
dataUsReq = rwMotorLog.motorTorque
dataSigmaBR = attErrorLog.sigma_BR
dataOmegaBR = attErrorLog.omega_BR_B
dataOmegaRW = mrpLog.wheelSpeeds
dataDH = deltaHLog.torqueRequestBody
dataMap = thrMapLog.thrForce
dataOnTime = onTimeLog.OnTimeRequest
dataRW = []
for i in range(numRW):
dataRW.append(rwLogs[i].u_current)
dataThr = []
for i in range(numTh):
dataThr.append(thrLog[i].thrustForce)
np.set_printoptions(precision=16)
# Displays the plots relative to the S/C attitude and rates errors, wheel momenta, thruster impulses, on times, and thruster firing intervals
timeData = rwMotorLog.times() * macros.NANO2SEC
plot_attitude_error(timeData, dataSigmaBR)
figureList = {}
pltName = fileName + "1"
figureList[pltName] = plt.figure(1)
plot_rate_error(timeData, dataOmegaBR)
pltName = fileName + "2"
figureList[pltName] = plt.figure(2)
plot_rw_momenta(timeData, dataOmegaRW, RW, numRW)
pltName = fileName + "3"
figureList[pltName] = plt.figure(3)
plot_DH(timeData, dataDH)
pltName = fileName + "4"
figureList[pltName] = plt.figure(4)
plot_thrImpulse(timeData, dataMap, numTh)
pltName = fileName + "5"
figureList[pltName] = plt.figure(5)
plot_OnTimeRequest(timeData, dataOnTime, numTh)
pltName = fileName + "6"
figureList[pltName] = plt.figure(6)
plot_thrForce(timeData, dataThr, numTh)
pltName = fileName + "7"
figureList[pltName] = plt.figure(7)
if show_plots:
plt.show()
# close the plots being saved off to avoid over-writing old and new figures
plt.close("all")
return figureList
# Plotting RW functions
[docs]
def plot_attitude_error(timeData, dataSigmaBR):
"""Plot the attitude errors."""
plt.figure(1)
for idx in range(3):
plt.plot(timeData, dataSigmaBR[:, idx],
color=unitTestSupport.getLineColor(idx, 3),
label=r'$\sigma_' + str(idx) + r'$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel(r'Attitude Error $\sigma_{B/R}$')
[docs]
def plot_rate_error(timeData, dataOmegaBR):
"""Plot the body angular velocity rate tracking errors."""
plt.figure(2)
for idx in range(3):
plt.plot(timeData, dataOmegaBR[:, idx],
color=unitTestSupport.getLineColor(idx, 3),
label=r'$\omega_{BR,' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('Rate Tracking Error (rad/s) ')
[docs]
def plot_rw_momenta(timeData, dataOmegaRw, RW, numRW):
"""Plot the RW momenta."""
totMomentumNorm = []
for j in range(len(timeData)):
totMomentum = np.array([0,0,0])
for idx in range(numRW):
for k in range(3):
totMomentum[k] = totMomentum[k] + dataOmegaRw[j, idx] * RW[idx].Js * RW[idx].gsHat_B[k][0]
totMomentumNorm.append(np.linalg.norm(totMomentum))
plt.figure(3)
for idx in range(numRW):
plt.plot(timeData, dataOmegaRw[:, idx] * RW[idx].Js,
color=unitTestSupport.getLineColor(idx, numRW),
label=r'$H_{' + str(idx+1) + r'}$')
plt.plot(timeData, totMomentumNorm, '--',
label=r'$\|H\|$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('RW Momentum (Nms)')
[docs]
def plot_DH(timeData, dataDH):
"""Plot the body angular velocity rate tracking errors."""
plt.figure(4)
for idx in range(3):
plt.plot(timeData, dataDH[:, idx],
color=unitTestSupport.getLineColor(idx, 3),
label=r'$\Delta H_{' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('Dumped momentum (Nms) ')
[docs]
def plot_rw_speeds(timeData, dataOmegaRW, numRW):
"""Plot the RW spin rates."""
plt.figure(5)
for idx in range(numRW):
plt.plot(timeData, dataOmegaRW[:, idx] / macros.RPM,
color=unitTestSupport.getLineColor(idx, numRW),
label=r'$\Omega_{' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('RW Speed (RPM) ')
[docs]
def plot_thrImpulse(timeDataFSW, dataMap, numTh):
"""Plot the Thruster force values."""
plt.figure(5)
for idx in range(numTh):
plt.plot(timeDataFSW, dataMap[:, idx],
color=unitTestSupport.getLineColor(idx, numTh),
label=r'$thrImpulse_{' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('Impulse requested [Ns]')
[docs]
def plot_OnTimeRequest(timeData, dataOnTime, numTh):
"""Plot the thruster on time requests."""
plt.figure(6)
for idx in range(numTh):
plt.plot(timeData, dataOnTime[:, idx],
color=unitTestSupport.getLineColor(idx, numTh),
label=r'$OnTimeRequest_{' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('OnTimeRequest [sec]')
[docs]
def plot_thrForce(timeDataFSW, dataThr, numTh):
"""Plot the Thruster force values."""
plt.figure(7)
for idx in range(numTh):
plt.plot(timeDataFSW, dataThr[idx],
color=unitTestSupport.getLineColor(idx, numTh),
label=r'$thrForce_{' + str(idx+1) + r'}$')
plt.legend(loc='lower right')
plt.xlabel('Time [min]')
plt.ylabel('Thruster force [N]')
if __name__ == "__main__":
run(True)