Advanced: Naming Effector States
A state effector adds integrated quantities to a spacecraft, such as a hinged panel’s angle and angular rate, a reaction wheel’s speed, or a fuel tank’s mass. The spacecraft’s C++ Module: dynParamManager stores these quantities as named states. Effectors supply default names, so you usually do not need to choose them yourself. The available name attributes and default naming conventions depend on the module.
Note
Read automatic names from the effector. Assign custom names when their exact text must repeat across simulation builds. Do not hard-code a generated numeric suffix or assume that each simulation starts its counter at one.
This tutorial uses C++ Module: hingedRigidBodyStateEffector to illustrate constructor-based automatic naming and optional custom names.
Which Names Identify States?
For a hinged panel, these attributes identify two different integrated states:
Attribute |
Quantity stored in the dynamics manager |
|---|---|
|
Panel angle, in radians |
|
Panel angular rate, in radians per second |
The Python variable panel refers to the effector object. Its ModelTag
labels the module for diagnostics. Neither renaming the Python variable nor
setting panel.ModelTag = "leftPanel" changes its state names.
After initializing the simulation, retrieve a state through its actual name:
sim.InitializeSimulation()
angle_state = vehicle.dynManager.getStateObject(panel.nameOfThetaState)
angle = angle_state.getState()[0][0] # [rad]
This avoids assuming a name such as "hingedRigidBodyTheta1". The state
storage does not exist until registration during initialization, even when the
name string is already available.
Automatic Names and Object Lifetimes
With constructor-based naming, the hinged-body module increments a static counter each time a panel is constructed. All instances of that module in the same Python process share the counter, including panels belonging to different spacecraft or different simulations. The counter records constructions, not the number of panels currently alive.
For example, assuming no earlier panel constructions, two panels receive
hingedRigidBodyTheta1 and hingedRigidBodyTheta2. The next panel receives
hingedRigidBodyTheta3, even if the first two panels have been destroyed.
Constructing a panel and then assigning custom names still advances this counter.
Important
Destroying an effector does not reset its naming counter. The built-in effectors that use constructor counters consistently leave them unchanged during destruction. New instances continue from the next counter value, even after previous instances have been destroyed.
del panel removes that Python reference; it does not necessarily destroy
the underlying effector immediately. A simulation or another object can still
hold a reference. Forcing gc.collect() is not a naming strategy: it does not
reset the counter or remove references that are still in use.
Creating New Simulations in One Python Process
Repeated builds mean creating new spacecraft and new effectors on each pass through a loop. For example, using the builder in the downloadable script:
for case in range(2):
sim = build_simulation(custom_names=False)
sim.InitializeSimulation()
sim.ExecuteSimulation()
del sim
The builder configures the stop time and physical parameters for each run.
Although these are new simulations, Python and the loaded Basilisk library
remain alive. Creating a new SimBaseClass does not reset static counters.
An illustrative sequence is:
Build |
Left panel’s angle-state name |
Right panel’s angle-state name |
|---|---|---|
First |
|
|
Second |
|
|
The starting suffix can be larger in a notebook or test session that has already
created panels. This does not make the states incorrect: each simulation can
look them up through its own panels’ name attributes. A lookup that assumes
"hingedRigidBodyTheta1" can fail in the second simulation.
This pattern occurs in parameter sweeps, optimization loops, tests, notebook
sessions, and Monte Carlo workers that execute several cases in one Python
process. Finishing a BSK run does not necessarily terminate its Python process.
A fresh Python interpreter starts with fresh library state; a forked worker
can inherit its parent’s counter state. Also, a Basilisk Process created
with CreateNewProcess() is a scheduling container, not a new Python process.
Resetting or continuing the same simulation is a separate operation. Keeping the same effector objects does not rerun their constructors or assign new constructor-based names. Restoring initial states, random seeds, and recorder contents is a separate concern from naming.
Choosing Repeatable Custom Names
Set the names inside the builder, before attaching the effectors and before initialization. Give each component a distinct, meaningful prefix:
left = hingedRigidBodyStateEffector.HingedRigidBodyStateEffector()
left.nameOfThetaState = "leftPanelAngle"
left.nameOfThetaDotState = "leftPanelRate"
right = hingedRigidBodyStateEffector.HingedRigidBodyStateEffector()
right.nameOfThetaState = "rightPanelAngle"
right.nameOfThetaDotState = "rightPanelRate"
Every call to the builder can use these same four strings. The user is responsible for keeping custom state names unique among all states in a shared dynamics manager, including the spacecraft hub and other effector types. Giving two panels the same angle-state name does not create two independent states. Independent spacecraft with separate managers can reuse the same names.
Assign every name whose exact text matters to your application. Setting only
nameOfThetaState leaves the angular-rate name automatic. Effectors may also
publish named properties, such as inertial position and attitude; these have
their own names and a separate property namespace. Renaming a state does not
rename its properties. Consult the module documentation for its setters and
attributes, and set custom property names before attaching dependent effectors.
Treat names as configuration: do not change them after initialization or after
another module has copied them for a connection.
Recording Data and Connecting Modules
Prefer an output-message recorder when the effector already publishes the quantity you need. For example, the panel’s output contains both angle and rate:
recorder = panel.hingedRigidBodyOutMsg.recorder()
sim.AddModelToTask("dynamicsTask", recorder)
The recorder can be configured before initialization and is independent of the state-name strings. For an internal state without a suitable message, retrieve its name from the effector rather than hard-coding a generated suffix.
With the hinged-panel setup above, state names are available during construction and remain unchanged during initialization. A connection configured during setup can therefore copy the name from its producing panel. If you use a custom name, assign it before making that connection.
Running the Example
From the repository root, with Basilisk available in your Python environment:
python docs/source/codeSamples/bsk-multiSim.py
The script creates two spring-loaded panels in each spacecraft and executes four independent simulations in one interpreter: two with automatic state names, then two with custom state names. It prints the angle and rate names for each panel, looks up the final states using those names, and reads the same quantities from message recorders. Only strings and numerical results are retained between runs.
The automatic-name rows have different suffixes between builds. The custom-name
rows repeat leftPanelAngle, leftPanelRate, rightPanelAngle, and
rightPanelRate. The physical configuration is identical in all four runs;
the example’s test checks that the final states agree with the message data and
with the other runs.
The builder and repeated-build loop are shown below. No explicit garbage collection or counter manipulation is needed.
def build_simulation(custom_names=False):
"""Build a spacecraft with two hinged panels and configure a short run.
:param custom_names: Assign repeatable angle and angular-rate state names.
:return: Simulation retaining its vehicle, panels, and message recorders.
"""
sim = SimulationBaseClass.SimBaseClass()
process = sim.CreateNewProcess("dynamics")
time_step = 0.05 # [s]
process.addTask(sim.CreateNewTask("dynamicsTask", macros.sec2nano(time_step)))
vehicle = spacecraft.Spacecraft()
vehicle.ModelTag = "vehicle"
vehicle.hub.mHub = 100.0 # [kg]
vehicle.hub.IHubPntBc_B = 100.0 * np.eye(3) # [kg m^2]
sim.AddModelToTask("dynamicsTask", vehicle)
panels = []
recorders = []
for index, label in enumerate(("leftPanel", "rightPanel")):
panel = hingedRigidBodyStateEffector.HingedRigidBodyStateEffector()
panel.ModelTag = label # A diagnostic label; this does not rename states.
if custom_names:
panel.nameOfThetaState = f"{label}Angle"
panel.nameOfThetaDotState = f"{label}Rate"
panel.mass = 1.0 # [kg]
panel.IPntS_S = np.eye(3) # [kg m^2]
panel.d = 0.5 # [m]
panel.k = 0.2 # [N m/rad]
panel.thetaInit = 0.1 * (index + 1) # [rad]
vehicle.addStateEffector(panel)
sim.AddModelToTask("dynamicsTask", panel)
# Record messages without depending on any generated state-name strings.
recorder = panel.hingedRigidBodyOutMsg.recorder()
sim.AddModelToTask("dynamicsTask", recorder)
panels.append(panel)
recorders.append(recorder)
# Retain access to the models and recorders for the caller's state lookups.
sim.vehicle = vehicle
sim.panels = panels
sim.recorders = recorders
duration = 0.5 # [s]
sim.ConfigureStopTime(macros.sec2nano(duration))
return sim
def run_cases():
"""Run two builds per naming choice and print the actual state names.
:return: Four dictionaries containing names and final state/message values.
Each numerical array has one row per panel and columns for angle [rad]
and angular rate [rad/s]. No simulation objects are retained.
"""
results = []
for custom_names in (False, True):
naming = "custom" if custom_names else "automatic"
print(f"\n{naming.capitalize()} state names: two fresh simulations")
print(f"{'Build':<7}{'Panel':<12}{'Angle state':<32}Rate state")
for case in range(1, 3):
sim = build_simulation(custom_names)
names_before = tuple(
(panel.nameOfThetaState, panel.nameOfThetaDotState)
for panel in sim.panels
)
sim.InitializeSimulation()
sim.ExecuteSimulation()
# Ask these panels for their names; do not guess numeric suffixes.
names = tuple(
(panel.nameOfThetaState, panel.nameOfThetaDotState)
for panel in sim.panels
)
for index, (angle_name, rate_name) in enumerate(names):
label = sim.panels[index].ModelTag
print(f"{case:<7}{label:<12}{angle_name:<32}{rate_name}")
final_states = np.array([
[sim.vehicle.dynManager.getStateObject(name).getState()[0][0]
for name in panel_names]
for panel_names in names
]) # Columns: angle [rad], angular rate [rad/s].
message_states = np.array([
[recorder.theta[-1], recorder.thetaDot[-1]]
for recorder in sim.recorders
]) # Columns: angle [rad], angular rate [rad/s].
print(f" Left panel final angle: {final_states[0, 0]:.6f} rad "
f"(message: {message_states[0, 0]:.6f} rad)")
results.append({
"custom_names": custom_names,
"names_before": names_before,
"names": names,
"states": final_states,
"messages": message_states,
})
# Ending a BSK run or dropping its Python reference does not reset
# the hinged-body module's static constructor counter.
del sim
return results