Skip to content

Commit 3572f8b

Browse files
Merge pull request #980 from AVSLab/feature/979-mujoco-stateful-sysmodel
Feature/979 mujoco stateful sysmodel
2 parents f358c44 + 8f775c8 commit 3572f8b

9 files changed

Lines changed: 318 additions & 24 deletions

File tree

docs/source/Support/bskReleaseNotes.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ Version |release|
7070
- Added support for showing ``QuadMap`` quadrilateral surface meshes in Vizard, with scenario :ref:`scenarioQuadMaps` detailing usage. Allows users to draw quads on celestial bodies and spacecraft.
7171
- Added ``fixedframe2lla()`` function in :ref:`vizSupport` which is useful for computing QuadMap mesh interpolations
7272
- Added QuadMap mesh support functions (:ref:`quadMapSupport`) for displaying camera FOV boxes as projected on the surface of a reference ellipsoid, and drawing rectangular latitude/longitude defined regions.
73-
73+
- :beta:`Mujoco Support`: Added ``StatefulSysModel`` for models in the dynamics task of ``MJScene`` that need to declare
74+
continuous-time states. Modified :ref:`scenarioDeployPanels` to illustrate the use of ``StatefulSysModel``.
7475

7576
Version 2.6.0 (Feb. 21, 2025)
7677
-------------------------------

examples/mujoco/scenarioDeployPanels.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
#. ``examples/mujoco/scenarioArmWithThrusters.py``
2323
2424
This script demonstrates how to simulate a spacecraft with solar panels deployed
25-
using a Proportional-Derivative (PD) controller. This script uses the MuJoCo-based
26-
:ref:`DynamicObject<dynamicObject>` :ref:`MJScene<MJScene>`.
25+
using a Proportional-Integral-Derivative (PID) controller. This script uses the
26+
MuJoCo-based :ref:`DynamicObject<dynamicObject>` :ref:`MJScene<MJScene>`.
2727
2828
In ``mujoco/scenarioArmWithThrusters.py``, we saw how we can constrain joints to
2929
follow a specific angle by letting the dynamic engine figure out and apply
3030
the necessary torques. In this script, we are controlling the joints using a
31-
PD controller. This is a more adequate simulation setup when you want to simulate
31+
PID controller. This is a more adequate simulation setup when you want to simulate
3232
or design the control system for these joints. It is also generally more
3333
computationally efficient than letting the dynamic engine figure out the torques.
3434
@@ -45,11 +45,13 @@
4545
plots and in the 3D visualization how the panels never get deployed over their
4646
joint limit.
4747
48-
The deployment of the panels is controlled using a Proportional-Derivative (PD)
49-
controller. The desired position and velocity profiles for the joints are
48+
The deployment of the panels is controlled using an analog PID controller.
49+
The desired position and velocity profiles for the joints are
5050
generated using a trapezoidal/triangular velocity profile. These profiles are
51-
then used as inputs to the PD controller, which computes the torque required to achieve
52-
the desired motion.
51+
then used as inputs to the PID controller, which computes the torque required to achieve
52+
the desired motion. Note that the controller class extends ``StatefulSysModel``,
53+
instead of ``SysModel``, since we need to register the integral error as a
54+
continuous state.
5355
5456
The simulation is run for 80 minutes and the state of the system is recorded.
5557
The desired and achieved joint angles, as well as the torque applied to each
@@ -65,7 +67,7 @@
6567
from Basilisk.simulation import mujoco
6668
from Basilisk.utilities import SimulationBaseClass
6769
from Basilisk.utilities import macros
68-
from Basilisk.architecture import sysModel
70+
from Basilisk.simulation import StatefulSysModel
6971
from Basilisk.architecture import messaging
7072
from Basilisk.simulation import svIntegrators
7173

@@ -252,7 +254,7 @@ def run(initialSpin: bool = False, showPlots: bool = False, visualize: bool = Fa
252254
# the measured position and velocity of the joint (in this case the
253255
# exact values are used, but in a real system these may be the product
254256
# of a sensor), and the output is the torque to be applied to the joint.
255-
pdController = PDController()
257+
pdController = PIDController()
256258
pdController.ModelTag = f"{actuatorName}_controller"
257259

258260
# Connect the interpolators to the PD controller for the desired
@@ -385,13 +387,17 @@ def run(initialSpin: bool = False, showPlots: bool = False, visualize: bool = Fa
385387

386388
# The following is an example of a Python-based SysModel that
387389
# can be added to the dynamics task of a MJScene.
388-
class PDController(sysModel.SysModel):
390+
class PIDController(StatefulSysModel.StatefulSysModel):
389391
"""
390-
A Proportional-Derivative (PD) Controller class for controlling joint states.
392+
A Proportional-Integral-Derivative (PID) Controller class for controlling joint states.
393+
394+
This models an analog PID controller, which means that its output evolves in continuous
395+
time, not discrete time. Thus, it should be used within the dynamics task of ``MJScene``.
391396
392397
Attributes:
393-
K (float): Proportional gain.
394-
P (float): Derivative gain.
398+
K_p (float): Proportional gain.
399+
K_d (float): Derivative gain.
400+
K_i (float): Integral gain.
395401
396402
measuredInMsg (messaging.ScalarJointStateMsgReader): Reader for the measured joint state.
397403
desiredInMsg (messaging.ScalarJointStateMsgReader): Reader for the desired joint state.
@@ -404,8 +410,9 @@ class PDController(sysModel.SysModel):
404410
def __init__(self, *args: Any):
405411
"""Initialize"""
406412
super().__init__(*args)
407-
self.K = 0.1
408-
self.P = 0.002
413+
self.K_p = 0.1
414+
self.K_d = 0.002
415+
self.K_i = 0.0001
409416

410417
self.measuredInMsg = messaging.ScalarJointStateMsgReader()
411418
self.desiredInMsg = messaging.ScalarJointStateMsgReader()
@@ -415,15 +422,23 @@ def __init__(self, *args: Any):
415422

416423
self.outputOutMsg = messaging.SingleActuatorMsg()
417424

425+
def registerStates(self, registerer: StatefulSysModel.DynParamRegisterer):
426+
self.integralErrorState = registerer.registerState(1, 1, "integralError")
427+
self.integralErrorState.setState([[0]]) # explicitely zero initialize
428+
418429
def UpdateState(self, CurrentSimNanos: int):
419430
"""Computes the control command from the measured and desired
420431
joint position and velocity."""
421432
# Compute the error in the state and its derivative
422433
stateError = self.desiredInMsg().state - self.measuredInMsg().state
423434
stateDotError = self.desiredDotInMsg().state - self.measuredDotInMsg().state
435+
stateIntegralError = self.integralErrorState.getState()[0][0]
424436

425437
# Compute the control output
426-
control_output = self.K * stateError + self.P * stateDotError
438+
control_output = self.K_p * stateError + self.K_d * stateDotError + self.K_i * stateIntegralError
439+
440+
# Set the derivative of the integral error inner state
441+
self.integralErrorState.setDerivative([[stateError]])
427442

428443
# Write the control output to the output message
429444
payload = messaging.SingleActuatorMsgPayload()

src/architecture/_GeneralModuleFiles/py_sys_model.i

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
%module(directors="1",threads="1") sysModel
2+
%module(directors="1",threads="1",package="Basilisk.architecture") sysModel
33
%{
44
#include "sys_model.h"
55
%}

src/simulation/mujocoDynamics/_GeneralModuleFiles/MJBody.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@ void MJBody::writeStateDependentOutputMessages(uint64_t CurrentSimNanos)
207207
}
208208
}
209209

210-
void MJBody::registerStates(DynParamManager& paramManager)
210+
void MJBody::registerStates(DynParamRegisterer paramManager)
211211
{
212-
this->massState = paramManager.registerState(1, 1, "mujocoBodyMass_" + this->name);
212+
this->massState = paramManager.registerState(1, 1, "mass");
213213
}
214214

215215
void MJBody::updateMujocoModelFromMassProps()

src/simulation/mujocoDynamics/_GeneralModuleFiles/MJBody.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "MJJoint.h"
3737
#include "MJObject.h"
3838
#include "MJSite.h"
39+
#include "StatefulSysModel.h"
3940

4041
/// @cond
4142
/**
@@ -252,13 +253,13 @@ class MJBody : public MJObject<mjsBody>
252253
void writeStateDependentOutputMessages(uint64_t CurrentSimNanos);
253254

254255
/**
255-
* @brief Registers the body's states with a dynamic parameter manager.
256+
* @brief Registers the body's states with a dynamic parameter registerer.
256257
*
257258
* Currently, only the mass of the body is considered a parameter.
258259
*
259-
* @param paramManager Reference to the dynamic parameter manager.
260+
* @param paramManager The dynamic parameter registerer.
260261
*/
261-
void registerStates(DynParamManager& paramManager);
262+
void registerStates(DynParamRegisterer paramManager);
262263

263264
/**
264265
* @brief Updates the MuJoCo model from the mass properties of the body.

src/simulation/mujocoDynamics/_GeneralModuleFiles/MJScene.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "MJScene.h"
2121

2222
#include "MJFwdKinematics.h"
23+
#include "StatefulSysModel.h"
2324

2425
#include "simulation/dynamics/_GeneralModuleFiles/svIntegratorRK4.h"
2526
#include "architecture/utilities/macroDefinitions.h"
@@ -79,7 +80,10 @@ void MJScene::initializeDynamics()
7980
this->actState = this->dynManager.registerState(1, 1, "mujocoAct");
8081

8182
for (auto&& body : this->spec.getBodies()) {
82-
body.registerStates(this->dynManager);
83+
body.registerStates(DynParamRegisterer(
84+
this->dynManager,
85+
"body_" + body.getName() + "_"
86+
));
8387
}
8488

8589
// Make sure the spec is compiled
@@ -89,6 +93,19 @@ void MJScene::initializeDynamics()
8993
if (!recompiled) {
9094
this->spec.configure();
9195
}
96+
97+
// Register the states of the models in the dynamics task
98+
for (auto[_, sysModelPtr] : this->dynamicsTask.TaskModels)
99+
{
100+
if (auto statefulSysModelPtr = dynamic_cast<StatefulSysModel*>(sysModelPtr))
101+
{
102+
statefulSysModelPtr->registerStates(DynParamRegisterer(
103+
this->dynManager,
104+
sysModelPtr->ModelTag.empty() ? std::string("model") : sysModelPtr->ModelTag
105+
+ "_" + std::to_string(sysModelPtr->moduleID) + "_"
106+
));
107+
}
108+
}
92109
}
93110

94111
void MJScene::UpdateState(uint64_t CurrentSimNanos)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
ISC License
3+
4+
Copyright (c) 2025, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
5+
6+
Permission to use, copy, modify, and/or distribute this software for any
7+
purpose with or without fee is hereby granted, provided that the above
8+
copyright notice and this permission notice appear in all copies.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
18+
*/
19+
20+
#ifndef STATEFUL_SYS_MODEL_H
21+
#define STATEFUL_SYS_MODEL_H
22+
23+
#include "simulation/dynamics/_GeneralModuleFiles/dynParamManager.h"
24+
#include "architecture/_GeneralModuleFiles/sys_model.h"
25+
26+
/**A short-lived class passed to StatefulSysModel for them to register
27+
* their states.
28+
*
29+
* This class serves two purposes. First, it adds a prefix to every state
30+
* name before registering it on the actual DynParamManager. This prevents
31+
* state name collisions between StatefulSysModel as long as the prefix
32+
* are unique. Second, it exponses only the registerState method from
33+
* the DynParamManager. This prevents StatefulSysModel from registering
34+
* properties or accessing the states of other models, which would allow for
35+
* information to flow between models without going through the message system.
36+
* If a model needs to access information from another model, it should do so
37+
* thorugh a message, not by sharing a state or property.
38+
*/
39+
class DynParamRegisterer
40+
{
41+
public:
42+
/** Constructor */
43+
DynParamRegisterer(DynParamManager& manager, std::string stateNamePrefix)
44+
: manager(manager)
45+
, stateNamePrefix(stateNamePrefix)
46+
{}
47+
48+
/** Creates and returns a new state, which will be managed by the
49+
* underlying ``DynParamManager``.
50+
*
51+
* The state name should be unique: registering two states with the
52+
* same name on this class will cause an error. Different StatefulSysModel
53+
* are allowed to use the same state name, however.
54+
*
55+
* This method may optionally be templated to create StateData of
56+
* subclasses of StateData.
57+
*/
58+
template <typename StateDataType = StateData,
59+
std::enable_if_t<std::is_base_of_v<StateData, StateDataType>, bool> = true>
60+
inline StateDataType* registerState(uint32_t nRow, uint32_t nCol, std::string stateName)
61+
{
62+
return this->manager.registerState<StateDataType>(
63+
nRow, nCol, this->stateNamePrefix + stateName
64+
);
65+
}
66+
67+
protected:
68+
DynParamManager& manager; ///< wrapped manager
69+
std::string stateNamePrefix; ///< prefix added to all registered state names
70+
};
71+
72+
/** A SysModel that has continuous-time states.
73+
*
74+
* StatefulSysModel are added on the dynamics task of an MJScene.
75+
* On its UpdateState method, a StatefulSysModel should call each state's
76+
* setDerivative method. This value will be used by the integrator to
77+
* update the state for the next integrator step.
78+
*
79+
* The sample code below shows how to get the current value of the state
80+
* and how to set its derivative. In this case, ``x`` would follow an
81+
* exponential trajectory:
82+
* \code{.cpp}
83+
* void UpdateState(uint64_t CurrentSimNanos) override {
84+
* auto x = this->xState->getState();
85+
* this->xState->setDerivative( x );
86+
* }
87+
* \endcode
88+
*/
89+
class StatefulSysModel : public SysModel
90+
{
91+
public:
92+
/** Default constructor */
93+
StatefulSysModel() = default;
94+
95+
/**Used to register states on the given DynParamRegisterer.
96+
*
97+
* The main purpose of this method is for this class to call
98+
* ``registerState`` on the registerer. Note that state names
99+
* should not be repeated within the same StatefulSysModel.
100+
*
101+
* \code{.cpp}
102+
* void registerStates(DynParamRegisterer& registerer) override {
103+
* this->posState = registerer.register(3, 1, "pos");
104+
* this->massState = registerer.register(1, 1, "mass");
105+
* // etc.
106+
* }
107+
* \endcode
108+
*
109+
*/
110+
virtual void registerStates(DynParamRegisterer registerer) = 0;
111+
};
112+
113+
#endif

0 commit comments

Comments
 (0)