Modeling Mercury Orbital Data (2024–2025) and NKTg Invariant in Erlang

I am implementing a deterministic numerical validation workflow in Erlang using Mercury orbital data (2024–2025).

The dataset includes:

  • Position X (meters)

  • Velocity V (m/s)

  • Mass M (kg)

  • Momentum P = M * V

  • Invariant quantity NKTg1 = X * P

From 2024 reference data:

NKTg1 ≈ 8.90e38

For 2025 validation, velocity is reconstructed algebraically:

V = NKTg1 / (X * M)

Observed average relative deviation versus measured 2025 values: ~1–2%.


Erlang Implementation

Erlang supports arbitrary precision integers, but floating-point numbers are IEEE double precision. Since magnitudes approach 10^38, I am evaluating precision trade-offs.

Example module:

-module(mercury).
-export([momentum/2, invariant/3, simulated_velocity/3, relative_error/3]).

-define(NKTG1, 8.90e38).

momentum(Mass, Velocity) ->
    Mass * Velocity.

invariant(Position, Mass, Velocity) ->
    Position * momentum(Mass, Velocity).

simulated_velocity(Position, Mass, Nktg1) ->
    Nktg1 / (Position * Mass).

relative_error(Simulated, Observed) ->
    ((Simulated - Observed) / Observed) * 100.

Example usage:

Position = 5.16e10,
Mass     = 3.30e23,
Observed = 5.34e4,

SimV = mercury:simulated_velocity(Position, Mass, ?NKTG1),
Error = mercury:relative_error(SimV, Observed).

Observations

  1. Double precision handles ~10^38 magnitudes without overflow.

  2. Small rounding drift exists but remains within ~1–2%.

  3. Erlang’s immutable model keeps computations predictable.

  4. Pure functional arithmetic makes invariant-based modeling straightforward.


Questions for the Community

  1. For repeated high-magnitude computations (~10^38), is Erlang double precision sufficient, or should I integrate a high-precision numeric library?

  2. Are there recommended strategies for deterministic numeric reproducibility in BEAM systems?

  3. For scaling to large time-series datasets, would you suggest:

    • ETS-based storage?

    • Stream processing?

    • NIF for heavy numeric loops?

  4. Any known performance pitfalls when repeatedly computing:

    Constant / (X * M)
    

    at large scale in Erlang?

This is purely a numeric validation experiment focused on precision, determinism, and scalability in Erlang. I would appreciate insights from those experienced with scientific-style workloads on the BEAM VM.

2 Likes
  1. I would first apply nondimensionalization procedure to all values that go into the numerical integrator to bring their floating-point exponent as close to 0 as possible. This will alleviate some of of issues with doubles’ precision. Then go from there. Erlang’s integers are bigint, so, at the first glance, one may use fixed-point or rational arithmetic for this procedure and stay in pure Erlang. …Or just do floating point arithmetic for that purpose: loss of precision will occur once, rather than repeatedly. …Or just use your CAS to calculate the constants.
  2. I haven’t worked with floats in Erlang that much, but AFAIK they are regular machine floats under the hood (modulo error handling), so normal procedures should apply.
  3. These look like two separate questions: one about data structures, and another about NIFs. First question: generally, one should use ETS when data is accessed from multiple processes, otherwise local data structures (map or a list) may be faster. Second question: as usual, one should be extremely careful with NIFs, as they can mess up the BEAM scheduler, among other things. Before rolling your own, perhaps first consider the existing ones, like Elixir’s nx. (Disclaimer: I haven’t used that one)
  4. See 2.

I would appreciate insights from those experienced with scientific-style workloads on the BEAM VM.

Well, I haven’t done such things in BEAM, only in ol’ CUDA back in the day, so take this answer for what it’s worth.

1 Like

Some general things to consider regarding viability of Erlang in this setting:

  1. To answer whether double-precision math is sufficient for the task or not, I would study the papers about the concrete numerical integration algorithm that you plan to implement. I’ve got some experience with molecular dynamics, where we were exclusively after statistical properties of the systems, so regular conservative integrators sufficed. Generally, they run on doubles just fine. Orbital mechanics, as I imagine, is an entirely different beast, since it has to predict trajectories precisely, so I can’t say anything about your case.
  2. Floats in Erlang are boxed. For computations involving large arrays of floats (like in molecular dynamic) this is likely a deal-breaker. But for orbital mechanics it’s most likely fine: from what little do I know about it, computations are more complex, but volume of data is much smaller.

The first thing in modelling any physical system is to get the physics right.
Then from your physics equations you can estimate the magnitude of the various
terms and determine how accurately you can measure some things and how
accurately you should measure others.

In this case, I note that the orbit of Mercury is a 3D problem.
Very approximately, the gravitational forces on Mercury are those of the Sun and
Jupiter.
Relative to the ecliptic, Mercury’s inclination is listed as 7.00 degrees and
Jupiter’s as 1.30 degrees. (And if you can’t get better figures than those,
you’re going to be limited to 5- or 6-digit accuracy.) So not only will Jupiter
be pulling Mercury towards the Sun sometimes and away from it others, Jupiter
will be pulling Mercury “up” sometimes and “down” others.
Relative to the Sun’s equator, Mercury’s orbit is tilted about -3.5 degrees
and Jupiter’s about -6.1 degrees. This might have mattered because Mercury
is close enough to feel the gravitational effects of the Sun’s equatorial
bulge, but it turns out the effect is only just measurable. The effect of
the other planets (principally Jupiter) is measurable, about 5.3 seconds of
precession per year. Are your data precise enough for that to matter?

Relativistic effects are about a tenth the size, so maybe you can ignore them.

The gravitational pull of Jupiter on Mercury is about 160 000 times weaker than
the pull of the Sun on Mercury, but if you’re working to 6 digits of precision or
better, that’s enough to matter.

The figures I’ve seen listed for Mercury’s distance to the Sun are
46.00 to 69.82 Gm. Hmm. Search search search:
perihelion 46 001 200 km
aphelion 69 816 900 km
semi-major 57 909 100 km
The source I found didn’t give error bounds, but the 00 endings are rather
suspicious. I reckon that with corresponding figures for Jupiter and
similarly precise figures for orbital inclination you might be able to get
6 digit accuracy, but it’s going to take care.

What this means for your project is that provided you can avoid catastrophic
cancellation – which shouldn’t be too hard – double precision floating
point is going to be plenty of precision for your calculations.

It’s often useful to choose your units of measurement so that important
numbers come out close to 1. I remember being quite annoyed in relativity
classes with textbooks and papers saying “Choose units so that c = 1” but
honestly, it made sense. In this case, measuring mass in Jupiters,
length in astronomical units, and time in years should be a fair fit to
the Sun-Mercury-Jupiter system. I find 10 AU/year more comprehensible as
the average orbital speed of Mercury than 47872 m/sec. (10 AU/year?
Busy little beaver, isn’t it. 47872 km/sec? What’s that when it’s at home?)

The thing that got me thinking about this was your use of “Position” in some
of the formulas. This is a 3D problem. Position is not a number, it’s a
vector. Even in a model solar system confined to a plane, position would
still be a 2D vector, not a number. So I’m guessing that Position is supposed
to be Radius.

“Double precision handles ~10^38 magnitudes without overflow.”
You’re thinking of single precision.
Double precision goes up to 10^308.
With a sensible choice of units (Jupiters/AU/years), overflow shouldn’t be
a worry even in single precision.
What’s more important is the precision you get.
And that starts with “how much precision is there in your data” (with high
quality data, could be 6 or 7 digits), and ends with “how much precision do
you NEED to avoid numerical problems”.
Since Erlang offers you the choice of double precision or double precision,
you might as well choose double precision. That offers you about 53 bits
of precision or about 16 decimal digits, and unless you’re integrating over
ridiculously long periods (and you’re not) with ridiculously small time
steps (and I presume you’re not), and unless you subtract quantities that
are close in value (which you should take care not to), don’t worry.

“Are there recommended strategies for deterministic numeric reproducibility?”

Don’t use random or pseudo-random numbers. Seriously, reproducibility from what
to what? Different runs of the same compiled code? Just don’t go out of your way
to introduce nondeterminism. Runs of the same BEAM code on different architectures?
Sorry, I’m not sure even Java with strictfp guarantees or indeed can guarantee that.
Different compilations with different compiler versions or options? Keep the code
simple and parenthesise fully. And cross your fingers while touching wood and
praying to St Vidicon of Cathode.

“Scaling to large time series data sets”
How large is large? Seriously, how large? Seriously, data sets I used to think of as extremely
large now run in memory on an elderly laptop. And how fast are the data arriving, and how long
can you wait between when the data arrive and when you need the results?

“Pitfalls in computing Constant/(X * M)”

Make sure X and M are nonzero. Avoid underflow and overflow.
This is language-independent advice.

One thing I would like to suggest is that Erlang might not be the best language to prototype
this. A programming language that supports compile-time unit checking can catch a lot of nasty
mistakes when you’re doing physics calculations (including electronics). Consider this little
F# fragment:
let distance = 150.0
let time = 12.0

let speed : float<m/s> = distance / time

If the last expression were distance * time or distance + time, type checking would fail.
Units are compile-time only, imposing no run-time cost. I personally think F# is ugly
but this feature redeems it.