Creating DynamicObject Basilisk Modules

Basilisk modules that inherit from the class C++ Module: dynamicObject are still regular Basilisk modules that have the typical SelfInit(), Reset() and UpdateState() methods. However, they also contain a state engine called dynManager of the class DynParamManager and own a StateVecIntegrator. C++ Module: spacecraft is an example of a Basilisk module that is also inheriting from the DynamicObject class.

For the ownership, storage, and integrator-binding contracts behind this interface, see Dynamics state and integrator architecture.

In the spacecraft UpdateState() method the DynamicObject::integrateState() method is called. This call integrates all the registered spacecraft states, as well as all the connect state and dynamic effectors, to the next time step using the connected integrator type. See scenarioIntegrators for an example of setting the integrator on a dynamic object.

Note

Integrators connected to DynamicObject instances don’t have to be the same. It is possible to use an RK2 integrator on one spacecraft and the RK4 integrator on another.

Declare and initialize ODE states in the subclass’s Reset() implementation, then call finalizeStates() before integration. A typical Reset is:

void MyDynamicObject::Reset(uint64_t currentSimNanos)
{
    this->registerStates();
    this->dynManager.finalizeStates();
    this->linkStates();
    this->initializeAfterStateLink(currentSimNanos);
}

The first finalization allocates contiguous storage. Reacquire Eigen views after that allocation; the returned StateData handles themselves remain valid. Later resets reuse states by name and update their values in place. Their shapes, policies, and noise connections must remain compatible with the fixed layout. Repeated finalization has no effect. Initialization errors propagate to the caller without automatic value rollback.

The DynamicObject class contains two virtual methods preIntegration() and postIntegration(). The DynamicObject subclass must define what steps are to be completed before the integration step, and what post-integrations must be completed. For example, with C++ Module: spacecraft the pre-integration process determines the current time step to be integrated and stores some values used. In the post-integration step the MRP spacecraft attitude states are checked to not have a norm larger than 1 and the conservative DV component is determined.

Basilisk modules that are a subclass of DynamicObject are not restricted to mechanical integration scenarios as with the spacecraft example. See the discussion in Advanced: Using DynamicObject Basilisk Modules on how multiple Basilisk modules that inherit from the DynamicObject class can be linked. If linked, then the associated modules’ ordinary differential equations (ODEs) are integrated simultaneously.

Integrator ownership

A DynamicObject owns its active integrator through a C++ std::unique_ptr. In Python, use setIntegrator() or assign the integrator attribute:

scObject.setIntegrator(svIntegrators.svIntegratorRK4(scObject))

Both entry points transfer ownership to the dynamics object, so the Python integrator variable may go out of scope before the simulation runs. Existing scripts that call integrator.this.disown() or set integrator.thisown = False before installation remain supported; manual disowning is no longer necessary. Replacing the integrator destroys the previous one and preserves the primary object’s list of synchronized dynamics objects. Passing the active integrator again is a no-op. A newly supplied integrator that is rejected is destroyed; its Python proxy must not be reused.

The integrator attribute and getIntegrator() return borrowed access to the active integrator. That access is valid only until the integrator is replaced or its owning dynamics object is destroyed. Python synchronization connections retain their supplied secondary objects; see Lifetime of Synchronized Objects for the lifetime and configuration contract.

Custom C++ dynamics classes should use the setIntegrator(std::unique_ptr<StateVecIntegrator>) overload to transfer ownership explicitly. Include <memory> for std::make_unique and <utility> for std::move:

this->setIntegrator(std::make_unique<svIntegratorRK4>(this));

auto replacement = std::make_unique<svIntegratorRK4>(this);
this->setIntegrator(std::move(replacement));
// replacement is now empty; this dynamics object owns the integrator.

The owning argument is consumed even when validation rejects the replacement; the rejected integrator is destroyed and the active integrator remains installed. Replacing a synchronized secondary’s integrator logs a warning and discards the replacement. Change the primary’s integrator instead to preserve the group.

The raw-pointer overload remains available for Python and existing C++ callers, including setIntegrator(new svIntegratorRK4(this)) and the no-op when passing getIntegrator() back to the same object. Never construct a new unique_ptr from a borrowed integrator pointer. The C++ owning overload is hidden from Python.

The owning C++ integrator member is private. Replace direct assignments with setIntegrator() and use getIntegrator() when a borrowed raw pointer is required. Do not manually delete the owned integrator. Compiled extensions must be rebuilt against extension ABI version 3.

Create synchronization links through syncDynamicsIntegration() before the first integration step. The DynamicObject base destructor removes these links when either object is destroyed. It does not delete borrowed C++ dynamics objects. Use getIntegrationOwner() to inspect a connection and getDynamics() to inspect an integrator’s group. Once synchronization is configured, use setIntegrator() for replacements so that the new integrator receives the existing group.