The Core Code
Contents
The Core Code¶
The essential physics of Gravity Engine 2 is done by the GECore (GE) and GEPhysicsCore (GEPC) classes.
This section provides an overview of their architecture and usage. In very general terms GECore manages
the data and book-keeping and GEPhysicsCore does the physics evolution via numerical integration and/or propagators as
well as applying maneuvers as appropriate.
GECore¶
One of the key design objectives of the GECore (GEC) class was to ensure that it could be used as a
stand-alone engine to model orbital mechanics problems without references to Unity Monobehaviour classes. This ensures
that instances/copies can be used to run calculations for “what if” scenarios in e.g. path finding. A consequence of this
decision is that references to bodies in GECore are not directly tied to a GSBody object. All bodies in GEC are referred to by an
integer body identifier that is created when a body is added to GEC. (If a GSController is being used it manages the
recording of these ids in its interaction with GEC.)
Initializing¶
The constructor for GECore requires some key information for setting up the physics evolution that follows.
Once initialized these values cannot be altered.
public GECore(Integrators.Type integrator,
GBUnits.Units defaultUnits,
double orbitScale,
double massScale,
double stepsPerOrbit,
double startTimeJD = 0 // only needed if using SGP4
)
Currently GEC supports two numerical integration algorithms listed by the enum Integrator.Type.
Currently the choices are LEAPFROG and RK4. (Leapfrog integration is also called Verlet or Stormer-Verlet integration.
RK4 is an abbreviation of fourth-order Runge-Kutta integration.) Both integrators are capable and reasonably accurate
when given an appropriate step size for the intial conditions (more below).
Numerical integration is a deep and nuanced subject and useful overviews are presented in many orbital mechanics textbooks. My favorite currently is the discussion in Tremaine (2023).
Leapfrog is a simple second-order scheme that does a great job of conserving the energy of the system and it supports reversability. RK4 is a fourth order scheme that is slightly more accurate (it does four force evaluations per step vs leapfrog’s one) but does require more calculation.
The choice of a step size is the key decision in using an integrator. This decision is made more “intuitive” by the GEC parameters:
orbitScale, massScale and stepsPerOrbit. These are combined to compute a time step based on the period of the reference orbit
and the indicated number of steps per orbit.
orbitScale size of a typical circular orbit in the defaultUnits for the simulation
massScale mass of the central body for the typical orbit
stepsPerOrbit the number of integration steps to be computed for an orbit. For a circular orbit a number of about 200 is
reasonable. As the orbit becomes more eccentric the velocity at periapsis gets larger and more points are needed to ensure
smooth and accurate evolution.
startTimeJD In some cases the information for bodies in the system is specified at a specific world time, termed their epoch time. For example
the orbital elements for the International Space Station (ISS) in a TLE might be provided for Feb 15, 2024 at 11:30 UTC. Other bodies
may have different start times. The startTimeJD indicates the time at which GEC evolution is to start. This must be later than
any orbit element reference times since the orbit propagators only work for times after their epoch time. This time is specified
in Julian date format (see scaling for more).
Managing Bodies¶
Each body added to GECore has some common information (e.g. mass) and additional information about the
initial physical state. The simplest description of initial state is the position vector \(r\) and
velocity \(v\) in absolute world units. While simple, this leaves a lot of work to be done outside GEC
if the goal it have the initial state be e.g. “in a circular orbit around body with id=0”. Consequently
there are extension to this API to allow initial data to be provided in the form of classical orbit
elements (COE), relative RV, satellite data from a two-line element data set etc.
Bodies are added to GECore by using one of the variants of the BodyAdd API:
BodyAddBodyAddInOrbitWithRVRelativeBodyAddInOrbitWithCOEBodyAddInOrbitWithTLEBodyAddWithEphemeris
(see the code for the complete list of arguments). In each case the integer id GEC assignes to the body is returned by the API call.
The body add functions also may allow the specification of how the physics of the body is to be handled:
N-body gravity via numerical integration
algorithmic propagation around a center body (KEPLER, PKEPLER, SGP4)
use the body’s mass but do not move it (FIXED)
move according to a table of \((t, r, v)\) (EPHEMERIS)
These options correspond to an GEPC enum type GEPhysicsCore.Propagator.
Evolving a System of Bodies¶
The evolution of a system of bodies can be done directly with an in-line call or can be run on a worker thread using the Unity job system. The API calls for this are:
EvolveNow(double tUntilWorldrun evolution until the internal physics time is greater thantWorldUntil. (Due to the integrator timestep there may be a slight overshoot)Schedule(double tUntilWorldschedule a job to run the physics evolution. The job can be requested to complete by callingComplete(). The current completion status can be checked viaIsCompleted().
These calls evolve the system to tUntilWorld (the end time in world units). During this evolution the system will:
apply any maneuvers that have an execution time before the end time
detect collisions if colliders have been specified for bodies
handle propagation errors (e.g. SGP4 satellites may report they have decayed and can no longer evolve)
The API has variants of these API calls that include recording of the intermediate values of the state for a list of specified bodies.
In either case the core run loop is the same and is described in GEPhysicsCore
FIG: Core Run Loop
(phys-events)
Events¶
GECore provides feedback after physics evolution by sending events to any code that has registered to receive them
via the API:
public void PhysicsEventListenerAdd(PhysicsEventCallback physEventCB)
This triggers a callback to a function with the signature:
public delegate void PhysicsEventCallback(GECore ge, GEPhysicsCore.PhysEvent physEvent);
In which the event for the callback is defined as:
public struct PhysEvent {
public EventType type;
public int bodyId; // body id of body affected by the collision
public int bodyId_secondary; // body id of the body it collided with
public double t; // time of event
public double3 r; // world position of cid at time of event
public double3 r_secondary; // position of secondary body at time of event
public double3 v; // world position of cid at time of event
public double3 v_secondary; // position of secondary body at time of event
public CollisionType collisionType;
public int auxIndex; // aux index (collider entry, maneuver list entry etc.)
public int statusCode; // status code. SGP4 status/Boostwer status
With the type of events defined as:
public enum EventType { COLLISION, SGP4_ERROR, BOOSTER, KEPLER_ERROR, PATCH_CHANGE,
MANEUVER_ERROR, MANEUVER, EXTERNAL_ACCELERATION_REMOVED,
PKEPLER_ERROR, EA_REENTRY };
The generation of these events is summarized in the following table:
Event Type |
Description |
|---|---|
COLLISION |
|
SGP4_ERROR |
|
BOOSTER |
|
KEPLER_ERROR |
|
PATCH_CHANGE |
|
MANEUVER_ERROR |
|
MANEUVER |
|
EXTERNAL_ACCELERATION_REMOVED |
|
PKEPLER_ERROR |
|
EA_REENTRY |
Maneuvers and Physics State Changes¶
GEPhysicsCore¶
GEPhysicsCore (GEPC) is where all the physics (including propagators) happens. It contains the numerical integration,
propagator invocation, maneuver and collision detection code. The code is written to allow both direct execution
and execution as part of the Unity Job system. The Job system imposes strong limitations on data types (e.g. no class
references) to ensure it can operate in a thread-safe manner.
Main Physics Loop¶
The main physics loop of GEPhysicsCore is contained in the RunStep method. At a high level the sequence is:
perform numerical integration (Leapfrog, RK4 or circular restricted RK4). This is described in more detail in the paragraph below.
run any external accelerations that have dedicated/built-in integrators (e.g.
Booster)update all patched on-rails bodies to the patch for the current time
run each non-gravity propagator
The numerical integration loop computes an acceleration for each of the massive and massless bodies using a GRAVITY
propagator. The force resulting from massive Kepler bodies and external acceleration is also used to update the
acceleration value. The Leapfrog integrator maintains a value of acceleration from the previous step. This means that when
a body is added an initial pre-compute should be done to setup the past value of acceleration.
In some cases the evolution using an external acceleration requires integration on a finer timestep than is used for the
overall evolution (e.g. a rocket launch from the surface in which updates every 0.1 sec might be appropriate). Rather than force the entire simulation to move to this fine grained timestep (i.e. all satellites in orbit), GEPhysicsCore handles external
acclerations labeled as SELF_INTEGRATED as self-contained force computations. In these cases the entire set of forces acting
on the body are contained in the self-integrating external acceleration.
“Headless” GE and Recording Output¶
The GECore class can be used directly with scripting to model and evolve specific scenarios without
the scene component utility methods that simplify “in-scene” use of GE2. This section provides details on the
scripting setup required and illustrates the use of a non-interactive gravitational evolution and the recording
of the output and after-the-fact displaying.
The example scene BasicRecord and the controller RecordController provide an example of headless GE operation.
To make the result easier to interpret the recorder does use line renderers to display the recorded paths after GE
has run to completion.
The first thing the controller does as part of its Start() is to create and initialize a GECore:
double mu = 1000.0;
ge = new GECore(integrator: Integrators.Type.RK4,
defaultUnits: GBUnits.Units.DL,
orbitScale: 100.0,
massScale: mu,
stepsPerOrbit: 1000.0,
immediateMode: true);
Next bodies can be added to this GECore. To evolve the core and record information about specific bodies we
will use either GECore.EvolveNowRecordOutput or GECore.ScheduleRecordOutput depending on wheter we want immediate
evolution of the use of the job system. In both cases the API is:
public void EvolveNowRecordOutput(double tUntilWorld, double[] timePoints, int[] bodies)
tUntilWorldis the end world time for the simulationtimePointsis an array of time values for which output is to be recordedbodiesis an array of the body ids to be recorded
Once the evolution has completed the data is retreived by the GECore.RecordedOutputForBody call:
GEBodyState[] recordedOutput = ge.RecordedOutputForBody(bodyId, worldUnits: true);
This array contains the state information at the requested times.