Skip to content

Commit ddec43c

Browse files
committed
[#1490] Add zeroBase origin support to planetEphemeris
Add an optional zeroBase setting to PlanetEphemeris so generated planet states can be expressed relative to another body propagated by the same module instance. Preserve the existing heliocentric behavior when zeroBase is left empty. Resolve the selected body during Reset using case-insensitive name matching and report an initialization error when the requested body is not configured in the module. Compute all heliocentric states before subtracting the selected body's position and velocity, ensuring the translation is independent of body ordering. Leave planet names, timestamps, attitudes, attitude rates, and orientation flags unchanged. Expand the PlanetEphemeris unit tests to verify: - the default empty zeroBase value - position and velocity translation at multiple simulation times - zero position and velocity for the selected base body - preservation of attitude and message metadata - case-insensitive body-name matching - rejection of bodies from outside the module instance Document zeroBase configuration, default behavior, and the requirement that the reference body be propagated by the same PlanetEphemeris instance. Clarify that changing the output origin does not introduce hierarchical or mutually coupled orbital propagation. Update scenarioCustomGravBody to use Itokawa as the output origin, placing the planet and spacecraft states in a common asteroid-centered translational frame.
1 parent 5169b64 commit ddec43c

5 files changed

Lines changed: 326 additions & 25 deletions

File tree

examples/scenarioCustomGravBody.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@
5353
be customized. The gravity body ephemeris states are connected to the :ref:`planetEphemeris` planet
5454
state output messages.
5555
56-
Finally, the recorded states will all be relative to the inertial origin at the sun. :ref:`planetEphemeris` does not
57-
have the ``zeroBase`` capability as :ref:`spiceInterface` has. This script also records the asteroid
58-
states so that the plot is done of the spacecraft motion relative to the asteroid.
56+
The ephemeris outputs remain heliocentric because the Sun gravity body is not
57+
connected to an ephemeris message. Its default zero state therefore represents
58+
the Sun at the heliocentric origin. Setting ``planetEphemeris.zeroBase`` to
59+
Itokawa would incorrectly place both the Sun and Itokawa at the origin. To plot
60+
the spacecraft motion relative to the asteroid, the script instead records the
61+
Itokawa state and subtracts it from the spacecraft state during post-processing.
5962
6063
The simulation executes and shows a plot of the spacecraft motion relative to the asteroid.
6164
@@ -148,6 +151,9 @@ def run(show_plots):
148151

149152
# specify celestial object orbit
150153
gravBodyEphem.planetElements = planetEphemeris.classicElementVector([oeAsteroid, oeEarth])
154+
# Keep these outputs heliocentric. The unlinked Sun gravity body below uses
155+
# its default zero state to represent the Sun at the heliocentric origin.
156+
# The asteroid state is subtracted from the spacecraft state when plotting.
151157
# specify celestial object orientation
152158
gravBodyEphem.rightAscension = planetEphemeris.DoubleVector([0.0 * macros.D2R, 0.0 * macros.D2R])
153159
gravBodyEphem.declination = planetEphemeris.DoubleVector([0.0 * macros.D2R, 0.0 * macros.D2R])

src/simulation/environment/planetEphemeris/_UnitTest/test_planetEphemeris.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,189 @@
4646
from Basilisk.architecture.bskLogging import BasiliskError
4747

4848

49+
def _make_zero_base_test_elements():
50+
"""Create two heliocentric element sets for the zero-base tests.
51+
52+
Returns:
53+
planetEphemeris.classicElementVector: Independent Earth and Moon-like
54+
heliocentric element sets.
55+
"""
56+
earthElements = planetEphemeris.ClassicElements()
57+
earthElements.a = orbitalMotion.SMA_EARTH * 1000.0 # [m]
58+
earthElements.e = 0.01 # [-]
59+
earthElements.i = 10.0 * macros.D2R # [deg] -> [rad]
60+
earthElements.Omega = 30.0 * macros.D2R # [deg] -> [rad]
61+
earthElements.omega = 20.0 * macros.D2R # [deg] -> [rad]
62+
earthElements.f = 90.0 * macros.D2R # [deg] -> [rad]
63+
64+
moonElements = planetEphemeris.ClassicElements()
65+
moonElements.a = 1.01 * orbitalMotion.SMA_EARTH * 1000.0 # [m]
66+
moonElements.e = 0.02 # [-]
67+
moonElements.i = 5.0 * macros.D2R # [deg] -> [rad]
68+
moonElements.Omega = 110.0 * macros.D2R # [deg] -> [rad]
69+
moonElements.omega = 220.0 * macros.D2R # [deg] -> [rad]
70+
moonElements.f = 180.0 * macros.D2R # [deg] -> [rad]
71+
72+
return planetEphemeris.classicElementVector(
73+
[earthElements, moonElements]
74+
)
75+
76+
77+
def _configure_zero_base_test_module(module):
78+
"""Configure a ``PlanetEphemeris`` module with two oriented bodies.
79+
80+
Args:
81+
module (planetEphemeris.PlanetEphemeris): Module to configure.
82+
"""
83+
module.setPlanetNames(
84+
planetEphemeris.StringVector(["earth", "moon"])
85+
)
86+
module.planetElements = _make_zero_base_test_elements()
87+
module.rightAscension = planetEphemeris.DoubleVector(
88+
[0.0, 10.0 * macros.D2R]
89+
) # [rad]
90+
module.declination = planetEphemeris.DoubleVector(
91+
[90.0 * macros.D2R, 80.0 * macros.D2R]
92+
) # [rad]
93+
module.lst0 = planetEphemeris.DoubleVector(
94+
[0.0, 30.0 * macros.D2R]
95+
) # [rad]
96+
module.rotRate = planetEphemeris.DoubleVector(
97+
[1.0e-5, 2.0e-5]
98+
) # [rad/s]
99+
100+
101+
def test_zero_base_translation():
102+
"""Verify that ``zeroBase`` translates positions and velocities only.
103+
104+
Validation Test Description
105+
---------------------------
106+
Two identical modules propagate the same oriented bodies. One retains the
107+
default heliocentric origin while the other selects Earth as ``zeroBase``.
108+
The relative outputs are compared with differences of the heliocentric
109+
outputs at three simulation times.
110+
111+
Test Parameter Discussion
112+
-------------------------
113+
Mixed-case ``"EaRtH"`` verifies case-insensitive body-name matching. The
114+
bodies use distinct orbits and attitudes so position, velocity, and
115+
orientation behavior are all observable.
116+
117+
Description of Variables Being Tested
118+
---------------------------------------
119+
The test checks ``PositionVector``, ``VelocityVector``, ``J20002Pfix``,
120+
``J20002Pfix_dot``, ``computeOrient``, and ``PlanetName``.
121+
"""
122+
simulation = SimulationBaseClass.SimBaseClass()
123+
process = simulation.CreateNewProcess("zeroBaseProcess")
124+
taskStep = 0.5 # [s]
125+
process.addTask(
126+
simulation.CreateNewTask(
127+
"zeroBaseTask",
128+
macros.sec2nano(taskStep),
129+
)
130+
)
131+
132+
heliocentricModule = planetEphemeris.PlanetEphemeris()
133+
heliocentricModule.ModelTag = "heliocentricPlanetEphemeris"
134+
assert heliocentricModule.zeroBase == ""
135+
_configure_zero_base_test_module(heliocentricModule)
136+
137+
relativeModule = planetEphemeris.PlanetEphemeris()
138+
relativeModule.ModelTag = "relativePlanetEphemeris"
139+
_configure_zero_base_test_module(relativeModule)
140+
relativeModule.zeroBase = "EaRtH"
141+
142+
simulation.AddModelToTask("zeroBaseTask", heliocentricModule, 10)
143+
simulation.AddModelToTask("zeroBaseTask", relativeModule, 10)
144+
145+
heliocentricRecorders = [
146+
message.recorder() for message in heliocentricModule.planetOutMsgs
147+
]
148+
relativeRecorders = [
149+
message.recorder() for message in relativeModule.planetOutMsgs
150+
]
151+
for recorder in heliocentricRecorders + relativeRecorders:
152+
simulation.AddModelToTask("zeroBaseTask", recorder, 0)
153+
154+
simulation.InitializeSimulation()
155+
simulationDuration = 1.0 # [s]
156+
simulation.ConfigureStopTime(macros.sec2nano(simulationDuration))
157+
simulation.ExecuteSimulation()
158+
159+
basePosition = heliocentricRecorders[0].PositionVector
160+
baseVelocity = heliocentricRecorders[0].VelocityVector
161+
positionTolerance = 1.0e-6 # [m]
162+
velocityTolerance = 1.0e-10 # [m/s]
163+
attitudeTolerance = 1.0e-14 # [-]
164+
for bodyIndex in range(2):
165+
expectedPosition = (
166+
heliocentricRecorders[bodyIndex].PositionVector - basePosition
167+
)
168+
expectedVelocity = (
169+
heliocentricRecorders[bodyIndex].VelocityVector - baseVelocity
170+
)
171+
np.testing.assert_allclose(
172+
relativeRecorders[bodyIndex].PositionVector,
173+
expectedPosition,
174+
rtol=0.0,
175+
atol=positionTolerance,
176+
)
177+
np.testing.assert_allclose(
178+
relativeRecorders[bodyIndex].VelocityVector,
179+
expectedVelocity,
180+
rtol=0.0,
181+
atol=velocityTolerance,
182+
)
183+
np.testing.assert_allclose(
184+
relativeRecorders[bodyIndex].J20002Pfix,
185+
heliocentricRecorders[bodyIndex].J20002Pfix,
186+
rtol=0.0,
187+
atol=attitudeTolerance,
188+
)
189+
np.testing.assert_allclose(
190+
relativeRecorders[bodyIndex].J20002Pfix_dot,
191+
heliocentricRecorders[bodyIndex].J20002Pfix_dot,
192+
rtol=0.0,
193+
atol=attitudeTolerance,
194+
)
195+
np.testing.assert_array_equal(
196+
relativeRecorders[bodyIndex].computeOrient,
197+
heliocentricRecorders[bodyIndex].computeOrient,
198+
)
199+
200+
assert relativeModule.planetOutMsgs[0].read().PlanetName == "earth"
201+
assert relativeModule.planetOutMsgs[1].read().PlanetName == "moon"
202+
203+
204+
def test_zero_base_requires_local_body():
205+
"""Verify that ``zeroBase`` rejects bodies owned by another instance.
206+
207+
Validation Test Description
208+
---------------------------
209+
A module configured with Earth and Moon selects Mars as its translational
210+
origin. Initialization must stop with a ``BasiliskError`` because Mars is
211+
not propagated by that module instance.
212+
"""
213+
simulation = SimulationBaseClass.SimBaseClass()
214+
process = simulation.CreateNewProcess("invalidZeroBaseProcess")
215+
taskStep = 0.5 # [s]
216+
process.addTask(
217+
simulation.CreateNewTask(
218+
"invalidZeroBaseTask",
219+
macros.sec2nano(taskStep),
220+
)
221+
)
222+
223+
module = planetEphemeris.PlanetEphemeris()
224+
_configure_zero_base_test_module(module)
225+
module.zeroBase = "mars"
226+
simulation.AddModelToTask("invalidZeroBaseTask", module)
227+
228+
with pytest.raises(BasiliskError, match="not one of the bodies configured"):
229+
simulation.InitializeSimulation()
230+
231+
49232
# Uncomment this line is this test is to be skipped in the global unit test run, adjust message as needed.
50233
# @pytest.mark.skipif(conditionstring)
51234
# Uncomment this line if this test has an expected failure, adjust message as needed.
@@ -274,6 +457,8 @@ def planetEphemerisTest(show_plots, setRAN, setDEC, setLST, setRate):
274457
# stand-along python script
275458
#
276459
if __name__ == "__main__":
460+
test_zero_base_translation()
461+
test_zero_base_requires_local_body()
277462
test_module(
278463
False, # show plots flag
279464
True, # setRAN

src/simulation/environment/planetEphemeris/planetEphemeris.cpp

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,27 @@
1717
1818
*/
1919
#include "simulation/environment/planetEphemeris/planetEphemeris.h"
20+
#include <algorithm>
21+
#include <cctype>
2022
#include <cstdio>
2123
#include <iostream>
2224
#include "architecture/utilities/astroConstants.h"
2325
#include "architecture/utilities/linearAlgebra.h"
2426
#include "architecture/utilities/macroDefinitions.h"
2527
#include "architecture/utilities/rigidBodyKinematics.h"
2628

29+
namespace {
30+
/*! Compare two planet names without regard to letter case. */
31+
bool planetNamesMatch(const std::string& firstName, const std::string& secondName)
32+
{
33+
return firstName.size() == secondName.size()
34+
&& std::equal(firstName.begin(), firstName.end(), secondName.begin(),
35+
[](unsigned char firstCharacter, unsigned char secondCharacter) {
36+
return std::tolower(firstCharacter) == std::tolower(secondCharacter);
37+
});
38+
}
39+
} // namespace
40+
2741

2842
/*! This constructor initializes the variables.
2943
*/
@@ -64,14 +78,30 @@ void PlanetEphemeris::setPlanetNames(std::vector<std::string> names)
6478

6579

6680

67-
void PlanetEphemeris::Reset(uint64_t CurrenSimNanos)
81+
void PlanetEphemeris::Reset(uint64_t CurrentSimNanos)
6882
{
69-
this->epochTime = CurrenSimNanos*NANO2SEC;
83+
this->epochTime = CurrentSimNanos*NANO2SEC;
7084

7185
/*! - do sanity checks that the vector arrays for planet names and ephemeris have the same length */
7286
if (this->planetElements.size() != this->planetNames.size()) {
73-
bskLogger.bskError("Only %lu planet element sets provided, but %lu plane names are present.",
74-
this->planetElements.size(), this->planetNames.size());
87+
bskLogger.bskError("Only %zu planet element sets provided, but %zu planet names are present.",
88+
this->planetElements.size(), this->planetNames.size());
89+
}
90+
91+
/*! - Resolve the optional translational origin from the bodies owned by this module. */
92+
this->zeroBaseIndex = -1;
93+
if (!this->zeroBase.empty()) {
94+
for (size_t c = 0; c < this->planetNames.size(); c++) {
95+
if (planetNamesMatch(this->zeroBase, this->planetNames.at(c))) {
96+
this->zeroBaseIndex = static_cast<int>(c);
97+
break;
98+
}
99+
}
100+
if (this->zeroBaseIndex < 0) {
101+
bskLogger.bskError(
102+
"PlanetEphemeris zeroBase '%s' is not one of the bodies configured in this module instance.",
103+
this->zeroBase.c_str());
104+
}
75105
}
76106

77107
/*! - See if planet orientation information is set */
@@ -84,28 +114,32 @@ void PlanetEphemeris::Reset(uint64_t CurrenSimNanos)
84114
}
85115

86116
if (computeAttitudeFlag) {
87-
/*! - check that the right number of planet local sideral time angles are provided */
117+
/*! - check that the right number of planet local sidereal time angles are provided */
88118
if (this->lst0.size() != this->planetNames.size()) {
89-
bskLogger.bskError("Only %lu planet initial principal rotation angles provided, but %lu planet names are present.",
90-
this->lst0.size(), this->planetNames.size());
119+
bskLogger.bskError(
120+
"Only %zu planet initial principal rotation angles provided, but %zu planet names are present.",
121+
this->lst0.size(), this->planetNames.size());
91122
}
92123

93124
/*! - check that the right number of planet polar axis right ascension angles are provided */
94125
if (this->rightAscension.size() != this->planetNames.size()) {
95-
bskLogger.bskError("Only %lu planet right ascension angles provided, but %lu planet names are present.",
96-
this->rightAscension.size(), this->planetNames.size());
126+
bskLogger.bskError(
127+
"Only %zu planet right ascension angles provided, but %zu planet names are present.",
128+
this->rightAscension.size(), this->planetNames.size());
97129
}
98130

99131
/*! - check that the right number of planet polar axis declination angles are provided */
100132
if (this->declination.size() != this->planetNames.size()) {
101-
bskLogger.bskError("Only %lu planet declination angles provided, but %lu planet names are present.",
102-
this->declination.size(), this->planetNames.size());
133+
bskLogger.bskError(
134+
"Only %zu planet declination angles provided, but %zu planet names are present.",
135+
this->declination.size(), this->planetNames.size());
103136
}
104137

105138
/*! - check that the right number of planet polar rotation rates are provided */
106139
if (this->rotRate.size() != this->planetNames.size()) {
107-
bskLogger.bskError("Only %lu planet rotation rates provided, but %lu planet names are present.",
108-
this->rotRate.size(), this->planetNames.size());
140+
bskLogger.bskError(
141+
"Only %zu planet rotation rates provided, but %zu planet names are present.",
142+
this->rotRate.size(), this->planetNames.size());
109143
}
110144
}
111145

@@ -133,6 +167,7 @@ void PlanetEphemeris::UpdateState(uint64_t CurrentSimNanos)
133167
double omega_NP_P[3]; // [r/s] angular velocity of inertial frame relative to planet frame in planet frame components
134168
double tilde[3][3]; // [] skew-symmetric matrix
135169
double theta_PN[3]; // [rad] 3-2-3 planet orientation coordinates
170+
std::vector<SpicePlanetStateMsgPayload> planetStates(this->planetNames.size());
136171

137172

138173

@@ -146,7 +181,7 @@ void PlanetEphemeris::UpdateState(uint64_t CurrentSimNanos)
146181
for(it = this->planetNames.begin(), c=0; it != this->planetNames.end(); it++, c++)
147182
{
148183
//! - Create new planet output message copy
149-
SpicePlanetStateMsgPayload newPlanet;
184+
SpicePlanetStateMsgPayload& newPlanet = planetStates.at(c);
150185
newPlanet = this->planetOutMsgs.at(c)->zeroMsgPayload;
151186
//! - specify planet name in output message
152187
std::snprintf(newPlanet.PlanetName, sizeof(newPlanet.PlanetName), "%s", it->c_str());
@@ -183,9 +218,25 @@ void PlanetEphemeris::UpdateState(uint64_t CurrentSimNanos)
183218
m33SetIdentity(newPlanet.J20002Pfix); // set default zero orientation
184219
}
185220

186-
//! - write output message
187-
this->planetOutMsgs.at(c)->write(&newPlanet, this->moduleID, CurrentSimNanos);
221+
}
222+
223+
/*! - Translate every position and velocity to the selected zero-base origin. */
224+
if (this->zeroBaseIndex >= 0) {
225+
double zeroBasePosition_N[3]; // [m] zero-base heliocentric position
226+
double zeroBaseVelocity_N[3]; // [m/s] zero-base heliocentric velocity
227+
v3Copy(planetStates.at(this->zeroBaseIndex).PositionVector, zeroBasePosition_N);
228+
v3Copy(planetStates.at(this->zeroBaseIndex).VelocityVector, zeroBaseVelocity_N);
229+
230+
for (auto& planetState : planetStates) {
231+
v3Subtract(planetState.PositionVector, zeroBasePosition_N, planetState.PositionVector);
232+
v3Subtract(planetState.VelocityVector, zeroBaseVelocity_N, planetState.VelocityVector);
233+
}
234+
}
188235

236+
/*! - Write the translated planet states after every absolute state is available. */
237+
for (size_t planetIndex = 0; planetIndex < planetStates.size(); planetIndex++) {
238+
this->planetOutMsgs.at(planetIndex)->write(
239+
&planetStates.at(planetIndex), this->moduleID, CurrentSimNanos);
189240
}
190241
return;
191242
}

src/simulation/environment/planetEphemeris/planetEphemeris.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#ifndef planetEphemeris_H
2121
#define planetEphemeris_H
2222

23+
#include <string>
2324
#include <vector>
2425
#include "architecture/_GeneralModuleFiles/sys_model.h"
2526

@@ -52,12 +53,15 @@ class PlanetEphemeris: public SysModel {
5253

5354
std::vector<double> rotRate; //!< [r/s] planet rotation rate
5455

56+
std::string zeroBase{}; //!< -- Output origin body; empty preserves heliocentric states
57+
5558
BSKLogger bskLogger; //!< -- BSK Logging
5659

5760
private:
5861
std::vector<std::string> planetNames; //!< -- Vector of planet names
5962
double epochTime; //!< [s] time of provided planet ephemeris epoch
60-
int computeAttitudeFlag; //!< -- flag indicating if the planet orienation information is provided
63+
int computeAttitudeFlag; //!< -- flag indicating whether planet orientation is provided
64+
int zeroBaseIndex = -1; //!< -- index of the body selected as the translational origin
6165
};
6266

6367

0 commit comments

Comments
 (0)