Skip to content

Commit b5242e1

Browse files
committed
Split EllipticalOrbit and HyperbolicOrbit classes
- Store semimajor axis instead of pericenterDistance in EllipticalOrbit - Store semiminor axis to avoid some sqrt calls during evaluation - Support zero and negative mean anomaly in hyperbolic orbits - Fix velocity calculation for hyperbolic orbits - Consolidate state vector to orbital elements calculations - Extend orbital elements calculation to support hyperbolic orbits - Add tests for orbital elements calculations - Fix hyperbolic orbital elements display in Qt info panel
1 parent 85a6749 commit b5242e1

File tree

11 files changed

+588
-361
lines changed

11 files changed

+588
-361
lines changed

src/celengine/astro.cpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// as published by the Free Software Foundation; either version 2
99
// of the License, or (at your option) any later version.
1010

11+
#include <algorithm>
1112
#include <cmath>
1213
#include <ostream>
1314
#include <utility>
@@ -91,6 +92,13 @@ static const astro::LeapSecondRecord LeapSeconds[] =
9192
};
9293

9394

95+
static inline void negateIf(double& d, bool condition)
96+
{
97+
if (condition)
98+
d = -d;
99+
}
100+
101+
94102
static celestia::util::array_view<astro::LeapSecondRecord> g_leapSeconds = LeapSeconds;
95103

96104

@@ -765,3 +773,95 @@ void astro::setLeapSeconds(celestia::util::array_view<astro::LeapSecondRecord> l
765773
{
766774
g_leapSeconds = leapSeconds;
767775
}
776+
777+
778+
astro::KeplerElements astro::StateVectorToElements(const Eigen::Vector3d& r,
779+
const Eigen::Vector3d& v,
780+
double mu)
781+
{
782+
constexpr double tolerance = 1e-9;
783+
784+
Eigen::Vector3d h = r.cross(v);
785+
double rNorm = r.norm();
786+
787+
KeplerElements result;
788+
789+
// Compute eccentricity
790+
Eigen::Vector3d evec = v.cross(h) / mu - r / rNorm;
791+
result.eccentricity = evec.norm();
792+
793+
// Compute inclination
794+
result.inclination = std::acos(std::clamp(h.y() / h.norm(), -1.0, 1.0));
795+
796+
// Normal vector (UnitY x h)
797+
Eigen::Vector3d nvec(h[2], 0, -h[0]);
798+
double nNorm = nvec.norm();
799+
800+
// compute longAscendingNode and argPericenter
801+
if (result.inclination < tolerance)
802+
{
803+
// handle face-on orbit: by convention Omega = 0.0
804+
if (result.eccentricity >= tolerance)
805+
{
806+
result.argPericenter = std::acos(evec.x() / result.eccentricity);
807+
negateIf(result.argPericenter, evec.z() >= 0.0);
808+
}
809+
}
810+
else
811+
{
812+
result.longAscendingNode = std::acos(nvec.x() / nNorm);
813+
negateIf(result.longAscendingNode, nvec.z() >= 0.0);
814+
if (result.eccentricity >= tolerance)
815+
{
816+
result.argPericenter = std::acos(std::clamp(nvec.dot(evec) / (nNorm * result.eccentricity), -1.0, 1.0));
817+
negateIf(result.argPericenter, evec.y() < 0.0);
818+
}
819+
}
820+
821+
// compute true anomaly
822+
double nu;
823+
if (result.eccentricity >= tolerance)
824+
{
825+
nu = std::acos(std::clamp(evec.dot(r) / (result.eccentricity * rNorm), -1.0, 1.0));
826+
negateIf(nu, r.dot(v) < 0.0);
827+
}
828+
else
829+
{
830+
if (result.inclination < tolerance)
831+
{
832+
// circular face-on orbit
833+
nu = std::acos(r.x() / rNorm);
834+
negateIf(nu, v.x() > 0.0);
835+
}
836+
else
837+
{
838+
nu = std::acos(std::clamp(nvec.dot(r) / (nNorm * rNorm), -1.0, 1.0));
839+
negateIf(nu, nvec.dot(v) > 0.0);
840+
}
841+
}
842+
843+
double s_nu;
844+
double c_nu;
845+
celmath::sincos(nu, s_nu, c_nu);
846+
847+
// compute mean anomaly
848+
double e2 = celmath::square(result.eccentricity);
849+
if (result.eccentricity < 1.0)
850+
{
851+
double E = std::atan2(std::sqrt(1.0 - e2) * s_nu,
852+
result.eccentricity + c_nu);
853+
result.meanAnomaly = E - result.eccentricity * std::sin(E);
854+
}
855+
else
856+
{
857+
double sinhE = std::sqrt(e2 - 1.0) * s_nu / (1.0 + result.eccentricity * c_nu);
858+
double E = std::asinh(sinhE);
859+
result.meanAnomaly = result.eccentricity * sinhE - E;
860+
}
861+
862+
// compute semimajor axis
863+
result.semimajorAxis = 1.0 / (2.0 / rNorm - v.squaredNorm() / mu);
864+
result.period = 2.0 * celestia::numbers::pi * std::sqrt(celmath::cube(std::abs(result.semimajorAxis)) / mu);
865+
866+
return result;
867+
}

src/celengine/astro.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,19 @@ namespace astro
320320
return astro::speedOfLight * n;
321321
}
322322
}
323-
}
323+
324+
struct KeplerElements
325+
{
326+
double semimajorAxis{ 0.0 };
327+
double eccentricity{ 0.0 };
328+
double inclination{ 0.0 };
329+
double longAscendingNode{ 0.0 };
330+
double argPericenter{ 0.0 };
331+
double meanAnomaly{ 0.0 };
332+
double period{ 0.0 };
333+
};
334+
335+
KeplerElements StateVectorToElements(const Eigen::Vector3d&,
336+
const Eigen::Vector3d&,
337+
double);
338+
} // end namespace astro

src/celengine/parseobject.cpp

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ ParseDate(const Hash* hash, const string& name, double& jd)
143143
* SemiMajorAxis or PericenterDistance is in kilometers.
144144
*/
145145
static std::unique_ptr<celestia::ephem::Orbit>
146-
CreateEllipticalOrbit(const Hash* orbitData,
147-
bool usePlanetUnits)
146+
CreateKeplerianOrbit(const Hash* orbitData,
147+
bool usePlanetUnits)
148148
{
149149

150150
// default units for planets are AU and years, otherwise km and days
@@ -153,78 +153,85 @@ CreateEllipticalOrbit(const Hash* orbitData,
153153
double timeScale;
154154
GetDefaultUnits(usePlanetUnits, distanceScale, timeScale);
155155

156+
astro::KeplerElements elements;
157+
158+
elements.eccentricity = orbitData->getNumber<double>("Eccentricity").value_or(0.0);
159+
if (elements.eccentricity < 0.0)
160+
{
161+
GetLogger()->error("Negative eccentricity is invalid.\n");
162+
return nullptr;
163+
}
164+
else if (elements.eccentricity == 1.0)
165+
{
166+
GetLogger()->error("Parabolic orbits are not supported.\n");
167+
return nullptr;
168+
}
169+
156170
// SemiMajorAxis and Period are absolutely required; everything
157171
// else has a reasonable default.
158-
double pericenterDistance = 0.0;
159-
std::optional<double> semiMajorAxis = orbitData->getLength<double>("SemiMajorAxis", 1.0, distanceScale);
160-
if (!semiMajorAxis.has_value())
172+
if (auto semiMajorAxisValue = orbitData->getLength<double>("SemiMajorAxis", 1.0, distanceScale); semiMajorAxisValue.has_value())
161173
{
162-
if (auto pericenter = orbitData->getLength<double>("PericenterDistance", 1.0, distanceScale); pericenter.has_value())
163-
{
164-
pericenterDistance = *pericenter;
165-
}
166-
else
167-
{
168-
GetLogger()->error("SemiMajorAxis/PericenterDistance missing! Skipping planet . . .\n");
169-
return nullptr;
170-
}
174+
elements.semimajorAxis = *semiMajorAxisValue;
175+
}
176+
else if (auto pericenter = orbitData->getLength<double>("PericenterDistance", 1.0, distanceScale); pericenter.has_value())
177+
{
178+
elements.semimajorAxis = *pericenter / (1.0 - elements.eccentricity);
179+
}
180+
else
181+
{
182+
GetLogger()->error("SemiMajorAxis/PericenterDistance missing from orbit definition.\n");
183+
return nullptr;
171184
}
172185

173-
double period = 0.0;
174186
if (auto periodValue = orbitData->getTime<double>("Period", 1.0, timeScale); periodValue.has_value())
175187
{
176-
period = *periodValue;
188+
elements.period = *periodValue;
189+
if (elements.period == 0.0)
190+
{
191+
GetLogger()->error("Period cannot be zero.\n");
192+
return nullptr;
193+
}
177194
}
178195
else
179196
{
180-
GetLogger()->error("Period missing! Skipping planet . . .\n");
197+
GetLogger()->error("Period must be specified in EllipticalOrbit.\n");
181198
return nullptr;
182199
}
183200

184-
auto eccentricity = orbitData->getNumber<double>("Eccentricity").value_or(0.0);
201+
elements.inclination = orbitData->getAngle<double>("Inclination").value_or(0.0);
185202

186-
auto inclination = orbitData->getAngle<double>("Inclination").value_or(0.0);
203+
elements.longAscendingNode = orbitData->getAngle<double>("AscendingNode").value_or(0.0);
187204

188-
double ascendingNode = orbitData->getAngle<double>("AscendingNode").value_or(0.0);
189-
190-
double argOfPericenter = 0.0;
191205
if (auto argPeri = orbitData->getAngle<double>("ArgOfPericenter"); argPeri.has_value())
192206
{
193-
argOfPericenter = *argPeri;
207+
elements.argPericenter = *argPeri;
194208
}
195209
else if (auto longPeri = orbitData->getAngle<double>("LongOfPericenter"); longPeri.has_value())
196210
{
197-
argOfPericenter = *longPeri - ascendingNode;
211+
elements.argPericenter = *longPeri - elements.longAscendingNode;
198212
}
199213

200214
double epoch = astro::J2000;
201215
ParseDate(orbitData, "Epoch", epoch);
202216

203217
// Accept either the mean anomaly or mean longitude--use mean anomaly
204218
// if both are specified.
205-
double anomalyAtEpoch = 0.0;
206219
if (auto meanAnomaly = orbitData->getAngle<double>("MeanAnomaly"); meanAnomaly.has_value())
207-
{
208-
anomalyAtEpoch = *meanAnomaly;
209-
}
220+
elements.meanAnomaly = *meanAnomaly;
210221
else if (auto meanLongitude = orbitData->getAngle<double>("MeanLongitude"); meanLongitude.has_value())
222+
elements.meanAnomaly = *meanLongitude - (elements.argPericenter + elements.longAscendingNode);
223+
224+
elements.inclination = celmath::degToRad(elements.inclination);
225+
elements.longAscendingNode = celmath::degToRad(elements.longAscendingNode);
226+
elements.argPericenter = celmath::degToRad(elements.argPericenter);
227+
elements.meanAnomaly = celmath::degToRad(elements.meanAnomaly);
228+
229+
if (elements.eccentricity < 1.0)
211230
{
212-
anomalyAtEpoch = *meanLongitude - (argOfPericenter + ascendingNode);
231+
return std::make_unique<celestia::ephem::EllipticalOrbit>(elements, epoch);
213232
}
214233

215-
// If we read the semi-major axis, use it to compute the pericenter
216-
// distance.
217-
if (semiMajorAxis.has_value())
218-
pericenterDistance = *semiMajorAxis * (1.0 - eccentricity);
219-
220-
return std::make_unique<celestia::ephem::EllipticalOrbit>(pericenterDistance,
221-
eccentricity,
222-
degToRad(inclination),
223-
degToRad(ascendingNode),
224-
degToRad(argOfPericenter),
225-
degToRad(anomalyAtEpoch),
226-
period,
227-
epoch);
234+
return std::make_unique<celestia::ephem::HyperbolicOrbit>(elements, epoch);
228235
}
229236

230237

@@ -310,7 +317,7 @@ CreateFixedPosition(const Hash* trajData, const Selection& centralObject, bool u
310317
{
311318
if (centralObject.getType() != SelectionType::Body)
312319
{
313-
GetLogger()->error("FixedPosition planetographic coordinates aren't valid for stars.\n");
320+
GetLogger()->error("FixedPosition planetographic coordinates are not valid for stars.\n");
314321
return nullptr;
315322
}
316323

@@ -770,7 +777,7 @@ CreateOrbit(const Selection& centralObject,
770777
return nullptr;
771778
}
772779

773-
return CreateEllipticalOrbit(orbitData, usePlanetUnits).release();
780+
return CreateKeplerianOrbit(orbitData, usePlanetUnits).release();
774781
}
775782

776783
// Create an 'orbit' that places the object at a fixed point in its

src/celengine/render.cpp

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,42 +1090,24 @@ void Renderer::renderOrbit(const OrbitPathListEntry& orbitPath,
10901090
if (cachedOrbit == nullptr)
10911091
{
10921092
double startTime = t;
1093-
#if 0
1094-
int nSamples = detailOptions.orbitPathSamplePoints;
1095-
#endif
10961093

10971094
// Adjust the number of samples used for aperiodic orbits--these aren't
10981095
// true orbits, but are sampled trajectories, generally of spacecraft.
10991096
// Better control is really needed--some sort of adaptive sampling would
11001097
// be ideal.
1101-
if (!orbit->isPeriodic())
1098+
if (orbit->isPeriodic())
1099+
{
1100+
startTime = t - orbit->getPeriod();
1101+
}
1102+
else
11021103
{
11031104
double begin = 0.0, end = 0.0;
11041105
orbit->getValidRange(begin, end);
11051106

11061107
if (begin != end)
11071108
{
11081109
startTime = begin;
1109-
#if 0
1110-
nSamples = (int) (orbit->getPeriod() * 100.0);
1111-
nSamples = std::clamp(nSamples, 100, 1000);
1112-
#endif
11131110
}
1114-
#if 0
1115-
else
1116-
{
1117-
// If the orbit is aperiodic and doesn't have a
1118-
// finite duration, we don't render it. A compromise
1119-
// would be to pick some time window centered at the
1120-
// current time, but we'd have to pick some arbitrary
1121-
// duration.
1122-
nSamples = 0;
1123-
}
1124-
#endif
1125-
}
1126-
else
1127-
{
1128-
startTime = t - orbit->getPeriod();
11291111
}
11301112

11311113
cachedOrbit = new CurvePlot(*this);

0 commit comments

Comments
 (0)