Skip to content

Fix moon orbit generation#6330

Open
sam52796 wants to merge 4 commits into
pioneerspacesim:masterfrom
sam52796:Fix_moon_orbit_generation
Open

Fix moon orbit generation#6330
sam52796 wants to merge 4 commits into
pioneerspacesim:masterfrom
sam52796:Fix_moon_orbit_generation

Conversation

@sam52796

Copy link
Copy Markdown
Contributor

Fixes #5885.

The procedural generation was going wrong when adding moons to planets whose masses were small compared to their star's mass, because calculating the Hill Radius involves a cube root of the mass ratio, and the fixed point algorithm couldn't handle it. The system generator therefore calculated a far too large orbital slice, and filled it with moons at huge orbits. Doing a bit of parameter scaling fixes this issue, but results in different numbers of moons being generated, and hence probably qualifies for a save bump.

The second commit is to fix binary star orbits in custom systems. The procedural generation appears to get binary star systems correct, but if they are added in the system designer, or by hand, parameters can be left to random chance, and then the stars don't line up in their orbits correctly. Binary stars should always be on opposite sides of the barycentre, i.e. at the furthest possible distance allowed by their orbits, such that a line drawn between the two stars passes through the barycentre. Without this fix, 55 Cancri (for example) has both stars on the same side of the barycentre, which is gravitationally impossible.

While the second commit isn't save-breaking in a technical sense, it could lead to players being stranded if the star they are heading towards gets re-oriented to a different point in the orbit, so it seems like a good idea to lump it in with other save-breaking changes.

@sam52796 sam52796 force-pushed the Fix_moon_orbit_generation branch from 9e6df76 to 42d5b1c Compare April 21, 2026 08:30
@sam52796

Copy link
Copy Markdown
Contributor Author

I realised that a better fix for the cube root issue was to do the scaling in the cube root function itself, so I've edited the first commit so that it should produce a better result for any caller, instead of relying on the caller to scale where appropriate.

While testing I found another issue which is that moons can be created with 0 kg mass, e.g. the first moon in Gliese 1, so I added a third commit to abort creating a planet/moon if it's going to have a mass of 0. This is probably also save breaking, since now some moons will no longer appear. Now that I think about it, this commit may belong more with my previous "Fix moon populations" pull request, so I can move it there if that's preferred.

Comment thread src/galaxy/CustomSystem.cpp Outdated

#include <map>

static const fixed FIXED_PI = fixed(103993, 33102); // XXX Duplication from StarSystemGenerator.cpp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be better moved to a common header such as StarSystemGenerator.h which is visible to both, instead of duplicating

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is nothing star system generation specific about the value of pi, I thought it would be better in a more generic include file. It took all of my self-control not to move the definition of pi to the file called Pi.h, but I settled eventually on fixed.h.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a future-proofing method, I'd request the constant be defined as fixedf::PI instead, as that allows it to have a different definition based on the precision of the fixed-point template specialization instead.

template<...>
class fixedf {
    static const fixedf PI;
};

template<>
constexpr fixed fixed::PI = fixed(..., ...);

template<>
constexpr fixed fixedf<48>::PI = fixedf<48>(...);

Comment thread src/fixed.h
}

static fixedf CubeRootOf(const fixedf &a)
static fixedf CubeRootOf(const fixedf &a_orig)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have time at current to fully dig into the codepath and analyse the numerical inaccuracy at play here (and the effects of this change on its stability) - however, I'd strongly prefer if the rescaling version of this function be moved into a separate function, e.g. fixedf::CubeRootOfRescale. The automatic rescaling may be undesired behavior in codepaths that operate on values with known input bounds, and I'd prefer it to be opt-in.

That being said, I appreciate your tackling of this issue and I'm looking forward to reviewing it in-depth!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you aware that the cube root function is only used in one other place, and right next to that place there is a comment implying that underflow is a persistent problem for the function? In fact, that comment is what gave me the idea to use the bit shifting approach in the first place, since it explains how to use bit shifting to fix the problem.

I say this because I think it's better not to clutter up the codebase with duplicate library functions where the caller is expected to know which version to call. I realise that sometimes it's unavoidable when you really don't want to risk breaking things, but this doesn't seem to be one of those times.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you aware that the cube root function is only used in one other place, and right next to that place there is a comment implying that underflow is a persistent problem for the function?

Yes - I wrote that comment.

I say this because I think it's better not to clutter up the codebase with duplicate library functions where the caller is expected to know which version to call.

I do agree that duplicating functionality is to be avoided. If this function has noticeable loss-of-precision relative to the original, then there is a material difference between the two functions and thus a reason to separate it as a separate entry point.

After having built a test harness for both versions of the function, the proposed implementation has a worst-case error magnitude increase of 20% when the while (a > fixedf(1024, 1)) branch is taken, due to discarding up to 24 bits of fractional precision.

The good news is that replacing that branch with if (a > fixedf(1024, 1)) x = a * fixedf(2, 3); as the initial estimate experimentally fixes a noticed issue where the x * x term can catastrophically overflow (with an input value of 0x4299E58D00000000L), while retaining the same error magnitude as the original version of the function.

When that is resolved, I agree with your intent that the implementation is a direct upgrade in terms of numerical accuracy and stability and retract my initial recommendation.

@sturnclaw sturnclaw Apr 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, the while (a > fixedf(1024, 1)) branch did not present any reduction in computation error (with regards to a double-precision floating-point cube root using std::pow) compared to computing the N-R method cube root of the original value directly. Additionally, this increase in error metric was observed in both 16.48 fixed-point as well as 32.32 fixed-point representations.

The only noticed outliers as mentioned above were specific values triggering an overflow that caused the first iteration of the method to wildly overshoot beyond the ability of 47 more iterations to converge. Changing the initial estimate of x to a rough estimate of the first iteration result allows large input values to nominally remain within the allowed limits of the fixed-point representation.

@sam52796 sam52796 force-pushed the Fix_moon_orbit_generation branch 3 times, most recently from 511484b to 1ec7fe8 Compare April 21, 2026 19:55

@sturnclaw sturnclaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After having dug into the behavior of the function across a wide range of input values, I'm happy to find the precision of the result is much improved as long as only a scant few fractional bits of precision are discarded.

I've left suggested changes based on my experimental findings - with all changes applied, the algorithm has effectively uniform accuracy across the entire input range in both 32.32 and 16.48 precision modes (until the value becomes too small for the existing code to convert to double-precision floating-point, at which case I cannot compute a reference value).

This typically represents an order of magnitude (or more) increase in accuracy at high input values (1,000+), and at extremely low values the shift trick is successfully gaining significant amounts of precision.

My original request to split the function was based on my intuition that the shift at high values would discard significant amounts of the fractional bits, and I'm happy to retract that feedback now that a way to avoid the loss of precision has been found.

Comment thread src/fixed.h Outdated
Comment thread src/fixed.h
Comment thread src/fixed.h Outdated
@sam52796 sam52796 force-pushed the Fix_moon_orbit_generation branch 2 times, most recently from 1898abd to 0d1f141 Compare April 22, 2026 10:09
@sam52796 sam52796 force-pushed the Fix_moon_orbit_generation branch from 0d1f141 to bd98906 Compare April 22, 2026 10:13
@sam52796 sam52796 force-pushed the Fix_moon_orbit_generation branch from bd98906 to c8b97ed Compare April 22, 2026 10:32
@sam52796

sam52796 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

I've made all of @sturnclaw's requested changes, I think. C++ templates and fixed point math are both things that I prefer to think about as little as possible, so the suggestions were mainly copied verbatim. It compiles though! And also seems to work in all the cases I tested.

Note that I moved the PI constant template change into its own commit because it felt like its own thang.

Also note that this pull request also fixes #6302.

@sturnclaw sturnclaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! I've left a few nitpick/sanity-check comments, but I consider this ready for squash. I'll merge it along with the next batch of save-bump PRs once we enter the full feature-development cycle for the year; I'd like to cut an interim save-compatible release with our current accumulated bugfixes + the OpenAL audio backend sometime around the end of the month.

// very crudely
static const fixed AU_SOL_RADIUS = fixed(305, 65536);
static const fixed AU_EARTH_RADIUS = fixed(3, 65536); // XXX Duplication from StarSystem.cpp
static const fixed FIXED_PI = fixed(103993, 33102); // XXX Duplication from StarSystem.cpp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: if this is duplicated, the file it is duplicated from should be updated to also use the new fixed::PI values.

SystemBodyData *star_B = &body->children[1]->bodyData;

if ((star_A->m_type >= SystemBodyType::TYPE_STAR_MIN) && (star_A->m_type <= SystemBodyType::TYPE_STAR_MAX) &&
(star_B->m_type >= SystemBodyType::TYPE_STAR_MIN) && (star_B->m_type <= SystemBodyType::TYPE_STAR_MAX)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can be more succinctly expressed as star_A->GetSuperType() == SystemBodyType::SUPERTYPE_STAR etc.

In general the fixup-at-load approach is good. Ideally an extension to the editor for creating binary (and trinary) orbits from multiple bodies would be nice, though I suspect that would involve non-trivial engineering to track and express that a set of bodies are intended to be in a binary orbit.

Comment thread src/fixed.h
static fixedf CubeRootOf(const fixedf &a_orig)
{
fixedf a = a_orig;
if (a == fixedf(0, 1)) return fixedf(0, 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanity-check: should this be a <= fixedf(0, 1) instead? I believe computing the N-th root of a negative number is invalid by definition until you start involving complex numbers...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Weird system where a planet's reference frame includes its star

3 participants