Skip to main content
This tutorial walks through scenarioMonteCarloAttRW.py, which demonstrates how to use Basilisk’s MonteCarlo.Controller framework to run multiple randomized attitude simulations with reaction wheels, collect data across all runs, and analyze results.
The underlying simulation in each Monte Carlo run is the RW attitude feedback scenario from scenarioAttitudeFeedbackRW.py. If you are not already familiar with single-run RW attitude control, review that scenario first.

What this scenario demonstrates

  • Instantiating the Controller class to manage a multi-run simulation
  • Defining statistical dispersions on initial conditions and model parameters
  • Attaching a RetentionPolicy to log messages across runs
  • Running all cases and loading results from disk
  • Re-running individual cases deterministically with saved random seeds
  • Optional Bokeh interactive visualization

Running the scenario

# Basic matplotlib run
python3 scenarioMonteCarloAttRW.py

# Delete Monte Carlo data files after plotting
python3 scenarioMonteCarloAttRW.py --delete-data

# Interactive Bokeh visualization (do not use --delete-data with this)
python3 scenarioMonteCarloAttRW.py --bokeh-server

Required imports

from Basilisk.utilities.MonteCarlo.Controller import Controller, RetentionPolicy
from Basilisk.utilities.MonteCarlo.Dispersions import (
    UniformEulerAngleMRPDispersion,
    UniformDispersion,
    NormalVectorCartDispersion,
    InertiaTensorDispersion,
)

Step-by-step walkthrough

1

Instantiate the Controller

The Controller is the top-level Monte Carlo manager. You tell it which Python functions create and execute the simulation.
NUMBER_OF_RUNS = 4

monteCarlo = Controller()
monteCarlo.setSimulationFunction(createScenarioAttitudeFeedbackRW)
monteCarlo.setExecutionFunction(executeScenario)
monteCarlo.setExecutionCount(NUMBER_OF_RUNS)
monteCarlo.setShouldDisperseSeeds(True)
monteCarlo.setShowProgressBar(True)
monteCarlo.setVerbose(True)

dirName = "montecarlo_test" + str(os.getpid())
monteCarlo.setArchiveDir(dirName)
  • setSimulationFunction takes a callable that returns a configured SimBaseClass.
  • setExecutionFunction takes a callable that runs the simulation (calls ExecuteSimulation()).
  • setArchiveDir sets the directory where all run data is saved as binary files.
2

Define dispersions

Dispersions specify how parameters vary between runs. Each dispersion maps to a dot-path string that addresses the target attribute inside the simulation object tree.
# Address paths into the simulation object tree
dispMRPInit  = 'TaskList[0].TaskModels[0].hub.sigma_BNInit'
dispOmegaInit = 'TaskList[0].TaskModels[0].hub.omega_BN_BInit'
dispMass     = 'TaskList[0].TaskModels[0].hub.mHub'
dispCoMOff   = 'TaskList[0].TaskModels[0].hub.r_BcB_B'
dispInertia  = 'hubref.IHubPntBc_B'
dispRW1Axis  = 'RW1.gsHat_B'
dispRW1Omega = 'RW1.Omega'
dispVoltageIO_0 = 'rwVoltageIO.voltage2TorqueGain[0]'

# Uniform random MRP attitude over full SO(3)
monteCarlo.addDispersion(UniformEulerAngleMRPDispersion(dispMRPInit))

# Normal dispersion on angular rate: mean=0, std=0.25 deg/s per axis
monteCarlo.addDispersion(
    NormalVectorCartDispersion(dispOmegaInit, 0.0, 0.75 / 3.0 * np.pi / 180)
)

# Uniform +/-5% on spacecraft mass (nominal 750 kg)
monteCarlo.addDispersion(
    UniformDispersion(dispMass, [750.0 - 0.05*750, 750.0 + 0.05*750])
)

# Normal CoM offset from theoretical position
monteCarlo.addDispersion(
    NormalVectorCartDispersion(
        dispCoMOff, [0.0, 0.0, 1.0], [0.05/3.0, 0.05/3.0, 0.1/3.0]
    )
)

# Rotational inertia tensor uncertainty (0.1 deg std rotation angles)
monteCarlo.addDispersion(InertiaTensorDispersion(dispInertia, stdAngle=0.1))

# RW spin axis misalignment (normal)
monteCarlo.addDispersion(
    NormalVectorCartDispersion(dispRW1Axis, [1.0, 0.0, 0.0],
                               [0.01/3.0, 0.005/3.0, 0.005/3.0])
)

# RW speed: uniform +/-5% around 100 RPM
monteCarlo.addDispersion(
    UniformDispersion(dispRW1Omega, [100.0 - 0.05*100, 100.0 + 0.05*100])
)

# Voltage-to-torque gain: uniform +/-5%
monteCarlo.addDispersion(
    UniformDispersion(dispVoltageIO_0, [0.2/10. - 0.05*0.2/10.,
                                        0.2/10. + 0.05*0.2/10.])
)
The dot-path addresses use the TaskList[i].TaskModels[j] indexing from the simulation’s internal task list. Objects also accessible by attribute name on scSim (such as RW1, hubref, rwVoltageIO) are addressed directly.
3

Define a retention policy

A RetentionPolicy specifies which message fields to log for every run and optionally attaches a plotting callback.
retentionPolicy = RetentionPolicy()

retentionPolicy.addMessageLog("guidMsg",     ["sigma_BR", "omega_BR_B"])
retentionPolicy.addMessageLog("transMsg",    ["r_BN_N"])
retentionPolicy.addMessageLog("rwSpeedMsg",  ["wheelSpeeds"])
retentionPolicy.addMessageLog("voltMsg",     ["voltage"])
retentionPolicy.addMessageLog("rwMotorTorqueMsg", ["motorTorque"])
for msgName in ["rw1Msg", "rw2Msg", "rw3Msg"]:
    retentionPolicy.addMessageLog(msgName, ["u_current"])

# Attach a matplotlib plotting callback
retentionPolicy.setDataCallback(plotSim)

monteCarlo.addRetentionPolicy(retentionPolicy)
4

Attach simulation objects to scSim

For the Monte Carlo framework to access and modify object parameters between runs, every object that has dispersions applied to it must be stored as an attribute on the scSim instance.
def createScenarioAttitudeFeedbackRW():
    scSim = SimulationBaseClass.SimBaseClass()
    scSim.dynProcess = scSim.CreateNewProcess(simProcessName)
    # ...
    scSim.scObject = spacecraft.Spacecraft()
    scSim.RW1 = RW1
    scSim.RW2 = RW2
    scSim.RW3 = RW3
    scSim.rwVoltageIO = motorVoltageInterface.MotorVoltageInterface()
    # ...
    return scSim
Without adding objects to scSim as attributes, the Monte Carlo framework cannot find them through the dot-path dispersion addresses and will not be able to apply dispersions or retain data correctly.
5

Execute all runs

failures = monteCarlo.executeSimulations()
assert len(failures) == 0, "No runs should fail"
The controller runs each case — potentially in parallel using multiple CPU cores — and writes results to the archive directory.
6

Load and inspect results

After execution, load the archive and pull retained data for any specific run.
monteCarloLoaded = Controller.load(dirName)

# Get retained data for the last run (index = NUMBER_OF_RUNS - 1)
retainedData = monteCarloLoaded.getRetainedData(NUMBER_OF_RUNS - 1)

# Access a specific message field across time
sigma_BR = retainedData["messages"]["guidMsg.sigma_BR"]

# Inspect the dispersed parameters used in a run
params = monteCarloLoaded.getParameters(NUMBER_OF_RUNS - 1)
print(params['TaskList[0].TaskModels[0].hub.mHub'])
7

Re-run a specific case

Because each run’s random seed is saved, you can reproduce any individual case exactly.
failed = monteCarloLoaded.reRunCases([NUMBER_OF_RUNS - 1])
assert len(failed) == 0

# Verify determinism: old and new outputs match
retainedData2 = monteCarloLoaded.getRetainedData(NUMBER_OF_RUNS - 1)
newOutput = retainedData2["messages"]["guidMsg.sigma_BR"]

Dispersion types reference

ClassDistributionTypical use
UniformEulerAngleMRPDispersionUniform on SO(3) via Euler angles → MRPInitial attitude
NormalVectorCartDispersionNormal per Cartesian componentAngular rate, CoM offsets, RW axes
UniformDispersionUniform scalar within boundsMass, RW speed, voltage gains
InertiaTensorDispersionRandom rotation of principal axes by small anglesInertia tensor uncertainty

Initial condition runs (case 2)

Beyond full Monte Carlo runs, the framework supports running from pre-saved initial conditions:
monteCarlo.setICDir(icName)
monteCarlo.setICRunFlag(True)
monteCarlo.setExecutionCount(numberICs)
failed = monteCarlo.runInitialConditions(runsList)
This is useful when you want to branch many runs from a single known simulation state.

Build docs developers (and LLMs) love