Advanced: Naming Effector States

On this page

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:

Hinged-panel state names

Attribute

Quantity stored in the dynamics manager

panel.nameOfThetaState

Panel angle, in radians

panel.nameOfThetaDotState

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:

Two panels constructed per simulation

Build

Left panel’s angle-state name

Right panel’s angle-state name

First

hingedRigidBodyTheta1

hingedRigidBodyTheta2

Second

hingedRigidBodyTheta3

hingedRigidBodyTheta4

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