Skip to content

Commit 3c0eef9

Browse files
committed
[#1490] Add gravity factory support for MuJoCo scenes
Extend gravBodyFactory.addBodiesTo() to configure MJScene objects in addition to conventional GravityEffector owners. For each MuJoCo scene, create an NBodyGravity model, register every factory body as a gravity source, and apply gravity independently at every MuJoCo body's center of mass. Retain the model and Vizard gravity metadata on the scene and reject duplicate gravity attachment. Allow NBodyGravity sources to retain a shared GravBodyData descriptor. Resolve the gravity model, central-body flag, and ephemeris input from the descriptor so the same configuration can be shared by conventional spacecraft and MuJoCo dynamics. Initialize descriptor-backed gravity models during Reset and warn when an orientation-dependent model has no ephemeris input. Expose descriptor-backed sources to Python as addGravitySourceFromBody(). Keep the existing manual addGravitySource(..., isCentralBody=...) interface unchanged, including support for its keyword argument. This enables gravity models and SPICE connections created by simIncludeGravBody to be reused directly by MuJoCo scenes without duplicating their configuration.
1 parent 6b9c222 commit 3c0eef9

4 files changed

Lines changed: 207 additions & 29 deletions

File tree

src/simulation/mujocoDynamics/NBodyGravity/NBodyGravity.cpp

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,105 @@
2020
#include "NBodyGravity.h"
2121

2222
#include "architecture/utilities/rigidBodyKinematics.h"
23+
#include "simulation/dynamics/_GeneralModuleFiles/gravityEffector.h"
24+
25+
namespace {
26+
std::shared_ptr<GravityModel>
27+
getGravityModel(const GravitySource& source)
28+
{
29+
return source.gravBody ? source.gravBody->gravityModel : source.model;
30+
}
31+
32+
bool
33+
isCentral(const GravitySource& source)
34+
{
35+
return source.gravBody ? source.gravBody->isCentralBody : source.isCentralBody;
36+
}
37+
38+
bool
39+
isStateLinked(GravitySource& source)
40+
{
41+
return source.stateInMsg.isLinked()
42+
|| (source.gravBody && source.gravBody->planetBodyInMsg.isLinked());
43+
}
44+
45+
SpicePlanetStateMsgPayload
46+
readState(GravitySource& source)
47+
{
48+
if (source.stateInMsg.isLinked())
49+
{
50+
return source.stateInMsg();
51+
}
52+
if (source.gravBody && source.gravBody->planetBodyInMsg.isLinked())
53+
{
54+
return source.gravBody->planetBodyInMsg();
55+
}
56+
throw BasiliskError("Cannot read an unlinked NBodyGravity source ephemeris message.");
57+
}
58+
} // namespace
2359

2460
void
2561
NBodyGravity::Reset(uint64_t CurrentSimNanos)
2662
{
27-
std::string errorPreffix = "In NBodyGravity '" + ModelTag + "': ";
63+
std::string errorPrefix = "In NBodyGravity '" + ModelTag + "': ";
2864
for (auto&& [name, target] : targets)
2965
{
3066
if (!target.centerOfMassStateInMsg.isLinked())
3167
{
32-
bskLogger.bskError("%sTarget '%s' centerOfMassStateInMsg is not linked!", errorPreffix.c_str(), name.c_str());
68+
bskLogger.bskError("%sTarget '%s' centerOfMassStateInMsg is not linked!", errorPrefix.c_str(), name.c_str());
3369
}
3470

3571
if (!target.massPropertiesInMsg.isLinked())
3672
{
37-
bskLogger.bskError("%sTarget '%s' massPropertiesInMsg is not linked!", errorPreffix.c_str(), name.c_str());
73+
bskLogger.bskError("%sTarget '%s' massPropertiesInMsg is not linked!", errorPrefix.c_str(), name.c_str());
3874
}
3975
}
4076

4177
bool foundCentralSource = false;
4278

4379
for (auto&& [name, source] : sources)
4480
{
45-
if (sources.size() > 1 && !source.stateInMsg.isLinked())
81+
if (sources.size() > 1 && !isStateLinked(source))
82+
{
83+
bskLogger.bskError(
84+
"%sSource '%s' has no ephemeris input but there are multiple gravity sources!",
85+
errorPrefix.c_str(),
86+
name.c_str());
87+
}
88+
89+
auto gravityModel = getGravityModel(source);
90+
if (!gravityModel)
4691
{
47-
bskLogger.bskError("%sSource '%s' stateInMsg is not linked but there are more than one sources!", errorPreffix.c_str(), name.c_str());
92+
bskLogger.bskError("%sSource '%s' has no gravity model!", errorPrefix.c_str(), name.c_str());
4893
}
4994

50-
auto error = source.model->initializeParameters();
95+
auto error = source.gravBody
96+
? gravityModel->initializeParameters(*source.gravBody)
97+
: gravityModel->initializeParameters();
5198
if (error)
5299
{
53-
bskLogger.bskError("%sWhile initializing source '%s' gravity model: %s", errorPreffix.c_str(), name.c_str(), error->c_str());
100+
bskLogger.bskError(
101+
"%sWhile initializing source '%s' gravity model: %s",
102+
errorPrefix.c_str(),
103+
name.c_str(),
104+
error->c_str());
54105
}
55106

56-
if (source.isCentralBody)
107+
if (gravityModel->dependsOnOrientation() && !isStateLinked(source))
108+
{
109+
bskLogger.bskLog(
110+
BSK_WARNING,
111+
"%sSource '%s' uses an orientation-dependent gravity model, but no "
112+
"ephemeris input is connected. The body will be treated as non-rotating.",
113+
errorPrefix.c_str(),
114+
name.c_str());
115+
}
116+
117+
if (isCentral(source))
57118
{
58119
if (foundCentralSource)
59120
{
60-
bskLogger.bskError("%sMore than one central body!", errorPreffix.c_str());
121+
bskLogger.bskError("%sMore than one central body!", errorPrefix.c_str());
61122
}
62123
foundCentralSource = true;
63124
}
@@ -91,7 +152,27 @@ NBodyGravity::UpdateState(uint64_t CurrentSimNanos)
91152
GravitySource&
92153
NBodyGravity::addGravitySource(std::string name, std::shared_ptr<GravityModel> gravityModel, bool isCentralBody)
93154
{
94-
auto[source, actuallyEmplaced] = this->sources.emplace(name, GravitySource{gravityModel, {}, isCentralBody});
155+
auto[source, actuallyEmplaced] =
156+
this->sources.emplace(name, GravitySource{gravityModel, {}, isCentralBody, nullptr});
157+
158+
if (!actuallyEmplaced)
159+
{
160+
bskLogger.bskError("Cannot use repeated source name: %s", name.c_str());
161+
}
162+
163+
return source->second;
164+
}
165+
166+
GravitySource&
167+
NBodyGravity::addGravitySource(std::string name, std::shared_ptr<GravBodyData> gravBody)
168+
{
169+
if (!gravBody)
170+
{
171+
bskLogger.bskError("Cannot add source '%s' from a null GravBodyData.", name.c_str());
172+
}
173+
174+
auto[source, actuallyEmplaced] =
175+
this->sources.emplace(name, GravitySource{nullptr, {}, false, std::move(gravBody)});
95176

96177
if (!actuallyEmplaced)
97178
{
@@ -149,21 +230,21 @@ NBodyGravity::addGravityTarget(std::string name, MJBody& body)
149230
Eigen::Vector3d
150231
NBodyGravity::computeAccelerationFromSource(GravitySource& source, Eigen::Vector3d r_J2000)
151232
{
152-
// Orientation and positon of the gravity source in J2000
233+
// Orientation and position of the gravity source in J2000
153234
Eigen::Matrix3d dcm_sourceFixedJ2000 = Eigen::Matrix3d::Identity();
154-
Eigen::Vector3d r_sourceJ200= Eigen::Vector3d::Zero();
155-
if (source.stateInMsg.isLinked())
235+
Eigen::Vector3d r_sourceJ2000 = Eigen::Vector3d::Zero();
236+
if (isStateLinked(source))
156237
{
157-
auto spicePayload = source.stateInMsg();
158-
dcm_sourceFixedJ2000 = c2DArray2EigenMatrix3d(spicePayload.J20002Pfix); // .transpose()
159-
r_sourceJ200 = cArray2EigenVector3d(spicePayload.PositionVector);
238+
auto spicePayload = readState(source);
239+
dcm_sourceFixedJ2000 = c2DArray2EigenMatrix3d(spicePayload.J20002Pfix);
240+
r_sourceJ2000 = cArray2EigenVector3d(spicePayload.PositionVector);
160241
}
161242

162243
// Transform position in J2000 reference frame to source-fixed reference frame
163-
Eigen::Vector3d r_sourceFix = dcm_sourceFixedJ2000 * (r_J2000 - r_sourceJ200);
244+
Eigen::Vector3d r_sourceFix = dcm_sourceFixedJ2000 * (r_J2000 - r_sourceJ2000);
164245

165246
// Compute the gravity acceleration on the source-fixed reference frame
166-
Eigen::Vector3d grav_sourceFix = source.model->computeField(r_sourceFix);
247+
Eigen::Vector3d grav_sourceFix = getGravityModel(source)->computeField(r_sourceFix);
167248

168249
// Convert gravity to J2000 reference frame
169250
return dcm_sourceFixedJ2000.transpose() * grav_sourceFix;
@@ -200,13 +281,13 @@ NBodyGravity::computeAccelerationOnTarget(GravityTarget& target)
200281

201282
for (auto&& [_, source] : sources)
202283
{
203-
if (!source.isCentralBody) continue;
284+
if (!isCentral(source)) continue;
204285

205286
centralSourceExists = true;
206287

207-
if (!source.stateInMsg.isLinked()) break;
288+
if (!isStateLinked(source)) break;
208289

209-
auto spicePayload = source.stateInMsg();
290+
auto spicePayload = readState(source);
210291
r_centralSourceSpiceRef_N = cArray2EigenVector3d(spicePayload.PositionVector);
211292
}
212293

@@ -228,7 +309,7 @@ NBodyGravity::computeAccelerationOnTarget(GravityTarget& target)
228309
// If there is a central body, and 'source' is not this one,
229310
// then we need to take into account the acceleration of the central
230311
// body due to the effect of this other source.
231-
if (centralSourceExists && !source.isCentralBody)
312+
if (centralSourceExists && !isCentral(source))
232313
{
233314
grav_N -= computeAccelerationFromSource(source, r_centralSourceSpiceRef_N);
234315
}

src/simulation/mujocoDynamics/NBodyGravity/NBodyGravity.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,20 @@
3434

3535
#include "simulation/mujocoDynamics/_GeneralModuleFiles/MJScene.h"
3636

37+
class GravBodyData;
38+
3739
/**
3840
* @brief Represents a gravity source in an N-body simulation.
3941
*
4042
* A gravity source is a celestial body or point mass that generates a gravitational field.
43+
* It can be configured manually or from a shared ``GravBodyData`` descriptor.
4144
*/
4245
struct GravitySource
4346
{
4447
std::shared_ptr<GravityModel> model; ///< The gravity model associated with the source.
4548
ReadFunctor<SpicePlanetStateMsgPayload> stateInMsg; ///< Input message providing the state of the source.
4649
bool isCentralBody; ///< Flag indicating whether this source is the central body.
50+
std::shared_ptr<GravBodyData> gravBody; ///< Optional canonical gravity-body descriptor.
4751
};
4852

4953
/**
@@ -102,6 +106,20 @@ class NBodyGravity : public SysModel
102106
*/
103107
GravitySource& addGravitySource(std::string name, std::shared_ptr<GravityModel> gravityModel, bool isCentralBody);
104108

109+
/**
110+
* @brief Adds a gravity source described by a ``GravBodyData`` object.
111+
*
112+
* The source retains the shared descriptor and reads its gravity model,
113+
* central-body flag, and ephemeris input live. During ``Reset()``, the
114+
* gravity model is initialized from the descriptor.
115+
*
116+
* @param name The name of the gravity source.
117+
* @param gravBody The canonical gravity-body descriptor.
118+
* @return A reference to the newly added ``GravitySource``.
119+
* @throw BasiliskError If ``gravBody`` is null or the source name is duplicated.
120+
*/
121+
GravitySource& addGravitySource(std::string name, std::shared_ptr<GravBodyData> gravBody);
122+
105123
/**
106124
* @brief Adds a new gravity target to the simulation.
107125
*

src/simulation/mujocoDynamics/NBodyGravity/NBodyGravity.i

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
%module NBodyGravity
2222
%{
2323
#include "NBodyGravity.h"
24+
#include "simulation/dynamics/_GeneralModuleFiles/gravityEffector.h"
2425
#include "simulation/dynamics/_GeneralModuleFiles/dynamicObject.h"
2526
#include "simulation/mujocoDynamics/_GeneralModuleFiles/MJInterpolators.h"
2627
%}
@@ -34,6 +35,14 @@ from Basilisk.architecture.swig_common_model import *
3435

3536
%import "simulation/mujocoDynamics/_GeneralModuleFiles/mujoco.i"
3637
%import "simulation/dynamics/_GeneralModuleFiles/gravityModel.i"
38+
%import "simulation/dynamics/gravityEffector/gravityEffector.i"
39+
40+
// Keep the existing manual-source function non-overloaded in Python so SWIG
41+
// retains support for calls that use the ``isCentralBody`` keyword argument.
42+
%rename(addGravitySourceFromBody) NBodyGravity::addGravitySource(
43+
std::string,
44+
std::shared_ptr<GravBodyData>
45+
);
3746

3847
%include "sys_model.i"
3948
%include "NBodyGravity.h"

src/utilities/simIncludeGravBody.py

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,88 @@ def __init__(self, bodyNames: Iterable[str] = []):
203203

204204
def addBodiesTo(
205205
self,
206-
objectToAddTheBodies: Union[gravityEffector.GravityEffector, WithGravField],
207-
):
208-
"""Can be called with a GravityEffector or an object that has a gravField
209-
variable to set the gravity bodies used in the object.
206+
objectToAddTheBodies: Any,
207+
) -> Optional[Any]:
208+
"""Attach the factory's gravity bodies to a dynamics object.
209+
210+
A conventional dynamics object may be a ``GravityEffector`` or expose a
211+
``gravField`` attribute. When an ``MJScene`` is provided, this method
212+
creates one ``NBodyGravity`` model, adds every factory body as a gravity
213+
source, and adds every MuJoCo body as a gravity target.
214+
215+
Args:
216+
objectToAddTheBodies (Any): Dynamics object that should receive the
217+
factory's gravity bodies.
218+
219+
Returns:
220+
Optional[Any]: The created ``NBodyGravity`` model for an ``MJScene``;
221+
otherwise, ``None``.
222+
223+
Note:
224+
When factory bodies use SPICE ephemerides, add the factory's
225+
``spiceObject`` to the ``MJScene`` dynamics task before the returned
226+
``NBodyGravity`` model. This keeps planet states current at each
227+
MuJoCo integrator substep.
210228
"""
211-
bodies = gravityEffector.GravBodyVector(list(self.gravBodies.values()))
212229
if isinstance(objectToAddTheBodies, WithGravField):
213230
objectToAddTheBodies = objectToAddTheBodies.gravField
214-
objectToAddTheBodies.setGravBodies(bodies) # type: ignore
231+
232+
setGravBodies = getattr(objectToAddTheBodies, "setGravBodies", None)
233+
if callable(setGravBodies):
234+
bodies = gravityEffector.GravBodyVector(list(self.gravBodies.values()))
235+
setGravBodies(bodies)
236+
return None
237+
238+
return self._addBodiesToMJScene(objectToAddTheBodies)
239+
240+
def _addBodiesToMJScene(self, scene: Any) -> Any:
241+
"""Attach the factory's gravity bodies to a MuJoCo scene."""
242+
try:
243+
from Basilisk.simulation import NBodyGravity
244+
from Basilisk.simulation import mujoco
245+
except ImportError as error:
246+
raise TypeError(
247+
"The supplied object does not support gravity-body attachment, "
248+
"and this Basilisk build does not include MuJoCo."
249+
) from error
250+
251+
if not isinstance(scene, mujoco.MJScene):
252+
raise TypeError(
253+
"addBodiesTo() requires a GravityEffector, an object with a "
254+
"gravField attribute, or an MJScene."
255+
)
256+
257+
cacheAttribute = "_gravBodyFactoryNBodyGravity"
258+
if getattr(scene, cacheAttribute, None) is not None:
259+
raise ValueError(
260+
f"Gravity bodies have already been added to MJScene '{scene.ModelTag}'."
261+
)
262+
263+
gravity = NBodyGravity.NBodyGravity()
264+
gravity.ModelTag = (
265+
f"{scene.ModelTag}_gravity" if scene.ModelTag else "mujocoScene_gravity"
266+
)
267+
268+
for name, gravBody in self.gravBodies.items():
269+
gravity.addGravitySourceFromBody(name, gravBody)
270+
271+
for name in scene.getBodyNames():
272+
gravity.addGravityTarget(name, scene.getBody(name))
273+
274+
scene.AddModelToDynamicsTask(gravity)
275+
276+
# MJScene stores raw model pointers in its dynamics task, so retain the
277+
# Python model for at least as long as the scene.
278+
setattr(scene, cacheAttribute, gravity)
279+
280+
# Match the standard spacecraft path, where Vizard discovers the
281+
# attached gravity bodies through the dynamics object.
282+
scene._vizGravBodies = self.gravBodies
283+
284+
return gravity
215285

216286
# Note, in the `create` functions below the `isCentralBody` and `useSphericalHarmParams` are
217-
# all set to False in the `GravGodyData()` constructor.
287+
# all set to False in the `GravBodyData()` constructor.
218288

219289
def createBody(
220290
self, bodyData: Union[str, BodyData]

0 commit comments

Comments
 (0)