J4 - J5 perturbations

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
I started the C++ coding, but I have a doubt. I see "import numpy as np"; do we need a multiprecision library to handle the numbers?

Probably not. The EGM96 model expresses the gravity perturbations in terms of coefficients of a class of functions called the 'fully-normalised Associated Legendre Functions'. As far as I can work out, the reason for doing this is to avoid overflow/underflow problems at high expansion orders when using normal double precision arithmetic. And if you are only going to consider the expansion terms up to orders less than, say, n=100 I doubt you will run into any problems. Nonetheless, I would be cautious if you try to use all n<=360 expansion terms.


Do you say "pseudo-inertial ECI" because the Earth barycenter accelerates (it's not truly inertial due to perturbations from the other celestial bodies) or do you mean something else?

That's basically what I mean. If we just take the Earth-Moon system, then the Earth rotates around the EMB at a radius of around 6,000 km completing one orbit every month or so and moving at an average speed of around 10 m/s. The ECI reference frame fixes the Earth at the centre of the coordinate system and so, relative to the EMB, it is not inertial because (in the simple Earth-Moon only model) a reference frame fixed at the EMB would be the natural origin for the inertial coordinate system. As you point out, to compensate for the different choice of coordinate origin in the ECI, one has to introduce a number of tidal forces for the Moon (and for the Sun in the Earth-Moon-Sun system) to compensate. But so long as you do include these forces in your ECI model, everything should be OK.


Since I use the SPICE library to load the initial state of all the celestial bodies, I do the conversions with SPICE.

OK. So long as you have a consistent way of converting between the non-rotating ECI and the rotating ECEF reference frames that should be fine - so long as the SPICE's ECEF coordinate frame lines up with EGM96's (i.e., because EGM96 assumes a longitudinal variation of the Earth's gravity field, Earth's prime meridian in SPICE needs to line up with EGM96's prime meridian.)
 

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
[...] so long as the SPICE's ECEF coordinate frame lines up with EGM96's (i.e., because EGM96 assumes a longitudinal variation of the Earth's gravity field, Earth's prime meridian in SPICE needs to line up with EGM96's prime meridian.)

SPICE can take into account many corrections when the high precision ITRF93 reference frame is used, but surely SPICE totally ignores any gravity model.
Isn't the prime meridian fixed, always the same?


Meanwhile, my C++ translation doesn't work. :-(

Is not clear to me whether the last 2 lines (P[n, n] = ... and DP[n, n]= ...) are inside or outside the for m cycle:

Code:
    for n in range(2, nmax + 1):
        for m in range(0, n):
            P[n, m]  = a[n][m] * t * P[n-1, m] - b[n][m] * P[n-2, m]
            DP[n, m] = a[n][m] * P[n-1, m] + a[n][m] * t * DP[n-1, m] - b[n][m] * DP[n-2, m]
        P[n, n]  = np.sqrt((2*n + 1) / 2 / n) * u * P[n-1, n-1]
        DP[n, n] = np.sqrt((2*n + 1) / 2 / n) * (u * DP[n-1, n-1] - t * P[n-1, n-1] / u)

I also see the "strange" syntax for P[n, m] and a[n][m]; is P[n, m] the same as C language P[n][m]?
 
Last edited:

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
SPICE can take into account many corrections when the high precision ITRF93 reference frame is used, but surely SPICE totally ignores any gravity model. Isn't the prime meridian fixed, always the same?

Yes, it is - but does the prime meridian of the SPICE's equivalent of an ECEF frame have a prime meridian that always passes through Greenwich? It should - but it's worth checking.


Is not clear to me whether the last 2 lines (P[n, n] = ... and DP[n, n]= ...) are inside or outside the for m cycle

Yes, they are outside the m-cycle, but inside the n-cycle.

Just a brief note on Python quirks:

Code:
for n in range(2, nmax + 1):
means: for 2 <=n <= nmax

and:

Code:
for m in range(0, n):
means: for 0 <=m <= n-1

---------- Post added at 01:06 AM ---------- Previous post was at 01:03 AM ----------

Code:
I also see the "strange" syntax for P[n, m] and a[n][m]; is P[n, m] the same as C language P[n][m]?

I've tried to maintain a C-style use of array indexation, but clearly I failed here. Yes, P[n, m] the same as C language P[n][m].

---------- Post added at 07:11 AM ---------- Previous post was at 01:06 AM ----------

Here is the C code that does the same job as the Python code.

Code:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int perturbations2 (double x,
                    double y,
                    double z,
                    double **C,
                    double **S,
                    double **a,
                    double **b,
                    double *f,
                    int    nmax) {
    
    // define the gravitational parameter and reference radius used in the EGM96 model
    double mu = 3986004.415e+8; //[M]^3[S]^-2
    double  R = 6378136.3;  //[M]
    
    double rho, r, t, u, k, phi, q;
    
    rho  = sqrt(x*x + y*y);
    r    = sqrt(x*x + y*y + z*z);
    t    = z / r;
    u    = sqrt(1 - t*t);
    k    = mu / r / r / r / r;
    phi  = R / r;
    
    // calculate the sin(m phi) and cos(m phi) for 0 <= m <= nmax
    double *cn = (double *) malloc((nmax+1) * sizeof(double));
    double *sn = (double *) malloc((nmax+1) * sizeof(double));
    
    cn[0] = 1.0;
    sn[0] = 0.0;
    
    for (int m = 1; m <= nmax; m++) {
        cn[m] = (x * cn[m-1] - y * sn[m-1]) / rho;
        sn[m] = (y * cn[m-1] + x * sn[m-1]) / rho;
    }
    
    
    // initialise the array used to store the fully-normalised associated Legendre functions
    double **P  = (double **) malloc((nmax+1) * sizeof(double *));
    double **DP = (double **) malloc((nmax+1) * sizeof(double *));
    
    for (int n = 0; n <= nmax; n++) {
        P[n]  = (double *) malloc((nmax+1) * sizeof(double));
        DP[n] = (double *) malloc((nmax+1) * sizeof(double));
    }
    
    P[0][0]  = 1.0;
    P[1][0]  = sqrt(3.0) * t;
    P[1][1]  = sqrt(3.0) * u;
    
    DP[0][0] = 0.0;
    DP[1][0] = sqrt(3.0);
    DP[1][1] =-sqrt(3.0) * t / u;
    
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m < n; m++) {
            P[n][m]  = a[n][m] * t * P[n-1][m] - b[n][m] * P[n-2][m];
            DP[n][m] = a[n][m] * P[n-1][m] + a[n][m] * t * DP[n-1][m] - b[n][m] * DP[n-2][m];
        }
        P[n][n]  = sqrt((double)(2*n + 1) / 2 / n) * u * P[n-1][n-1];
        DP[n][n] = sqrt((double)(2*n + 1) / 2 / n) * (u * DP[n-1][n-1] - t * P[n-1][n-1] / u);
    }
    
    
    // calculate the force components
    double ***FC  = (double ***) malloc((nmax+1) * sizeof(double **));
    double ***FS = (double ***) malloc((nmax+1) * sizeof(double **));
    
    for (int n = 0; n <= nmax; n++) {
        FC[n] = (double **) malloc((nmax+1) * sizeof(double *));
        FS[n] = (double **) malloc((nmax+1) * sizeof(double *));
        
        for (int m = 0; m <= nmax; m++) {
            FC[n][m] = (double *) malloc(3 * sizeof(double));
            FS[n][m] = (double *) malloc(3 * sizeof(double));
        }
    }

    q  = phi;
    for (int n = 2; n <= nmax; n++) {
        q *= phi;
        for (int m = 0; m <=n; m++) {
            FC[n][m][0] = -C[n][m]*k*q*(r*((n+1)*x*cn[m]-m*pow(r/rho, 2)*y*sn[m])*P[n][m]+x*z*cn[m]*DP[n][m]);
            FC[n][m][1] = -C[n][m]*k*q*(r*((n+1)*y*cn[m]+m*pow(r/rho, 2)*x*sn[m])*P[n][m]+y*z*cn[m]*DP[n][m]);
            FC[n][m][2] = -C[n][m]*k*q*(r* (n+1)*z*cn[m]*P[n][m]-(x*x + y*y)*cn[m]*DP[n][m]);
            
            FS[n][m][0] = -S[n][m]*k*q*(r*((n+1)*x*sn[m]+m*pow(r/rho, 2)*y*cn[m])*P[n][m]+x*z*sn[m]*DP[n][m]);
            FS[n][m][1] = -S[n][m]*k*q*(r*((n+1)*y*sn[m]-m*pow(r/rho, 2)*x*cn[m])*P[n][m]+y*z*sn[m]*DP[n][m]);
            FS[n][m][2] = -S[n][m]*k*q*(r* (n+1)*z*sn[m]*P[n][m]-(x*x + y*y)*sn[m]*DP[n][m]);
        }
    }

    
    // calculate the total force to be returned by the routine
    f[0] = 0.0; f[1] = 0.0; f[2] = 0.0;
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m <= n; m++) {
            f[0] += FC[n][m][0]; f[0] += FS[n][m][0];
            f[1] += FC[n][m][1]; f[1] += FS[n][m][1];
            f[2] += FC[n][m][2]; f[2] += FS[n][m][2];
        }
    }
    
    
    // free the allocated memory
    for (int n = 0; n <= nmax; n++) {
        for (int m = 0; m <= nmax; m++) {
            free (FC[n][m]); free (FS[n][m]);
        }
        free (FC[n]); free (FS[n]);
    }
    free (FC); free(FS);
    
    for (int n = 0; n <= nmax; n++) {
        free (P[n] ); free (DP[n]);
    }
    free (P); free (DP);
    
    free (cn); free (sn);
    
    
    // return error code zero
    return 0;
}

int main(int argc, const char * argv[]) {
    
    int     nmax = 12;
    
    double **C = (double **) malloc((nmax+1) * sizeof(double *));
    double **S = (double **) malloc((nmax+1) * sizeof(double *));
    
    for (int n = 0; n <= nmax; n++) {
        C[n] = (double *) malloc((nmax+1) * sizeof(double));
        S[n] = (double *) malloc((nmax+1) * sizeof(double));
    }
    
    
    // load the data from the EGM96 model
    FILE* file;
    int i, j, err;
    double dum1, dum2;
    
    file = fopen("-- path and filename for EGM96 data file --", "r");
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m <= n; m++) {
            fscanf(file , "%i %i %lf %lf %lf %lf \n" , &i, &j, &C[n][m], &S[n][m], &dum1, &dum2);
        }
    }
    fclose(file);
    
    
    // declare and calculaethe recursion parameters for the full-normalised Associate Legendre Polynomials
    double **a = (double **) malloc((nmax+1) * sizeof(double *));
    double **b = (double **) malloc((nmax+1) * sizeof(double *));
    
    for (int n = 0; n <= nmax; n++) {
        a[n] = (double *) malloc((nmax+1) * sizeof(double));
        b[n] = (double *) malloc((nmax+1) * sizeof(double));
    }
    
    for (int n = 1; n <= nmax; n++) {
        for (int m = 0; m < n; m++) {
            a[n][m] = sqrt((double)(4*n*n - 1) / (double)(n*n - m*m));
        }
    }
    
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m < n-1; m++) {
            b[n][m] = a[n][m] / a[n-1][m];
        }
        b[n][n-1] = 0.0;
    }
    
    
    // call the core perturbation problem
    double *f = (double *) malloc(3 * sizeof(double));
    err = perturbations2(3776163., 4500255., 3370373., C, S, a, b, f, nmax);
    
    printf("%2.10f\n", f[0]);
    printf("%2.10f\n", f[1]);
    printf("%2.10f\n", f[2]);
    

    
    // free memory
    free (f);
    
    for (int n = 0; n <= nmax; n++) {
        free (a[n]); free (b[n]);
    }
    free (a); free (b);
    
    for (int n = 0; n <= nmax; n++) {
        free (C[n]); free (S[n]);
    }
    free (C); free (S);
    
    
    // return error code 0
    return 0;
}

I've carried out a few spot checks of this against the Python code and it generates the same results in those instances. I suspect that any errors now are likely to be in the maths. But we will just have to see how what results you generate.
 
Last edited:

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
Yes, it is - but does the prime meridian of the SPICE's equivalent of an ECEF frame have a prime meridian that always passes through Greenwich? It should - but it's worth checking.

I really don't know how to check that, but if I use GeographicLib + EGM96 model, the result is in good agreement with SGP4.

I've carried out a few spot checks of this against the Python code and it generates the same results in those instances. I suspect that any errors now are likely to be in the maths. But we will just have to see how what results you generate.

I get:
0.0017397646
-552414593473969230000000000000000000000000000000000000000000000000000.0000000000
-552414593473969230000000000000000000000000000000000000000000000000000.0000000000

I fear that some memory allocation is not coherent with the for cycles.
 

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
I get:
0.0017397646
-55241459347396923000000000000000000000000000000000 0000000000000000000.0000000000
-55241459347396923000000000000000000000000000000000 0000000000000000000.0000000000

I fear that some memory allocation is not coherent with the for cycles.

For the time being, I've commented out the 'free' statements in the above C code. I'm getting:

Code:
0.0017397646
0.0018737419
-0.0108815201
Program ended with exit code: 0

Give me a few hours and I should be able to sort out the memory allocation problem.

---------- Post added at 10:47 AM ---------- Previous post was at 10:24 AM ----------

Memory allocation problem now fixed. Basically, the main routine wasn't releasing memory properly.

I've edited the above C code. Feel free to try again.
 

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
0.0017397646
-552414593473969230000000000000000000000000000000000000000000000000000.0000000000
-552414593473969230000000000000000000000000000000000000000000000000000.0000000000

I guess that the problem is in the memory allocation, not in the memory deallocation.

Meanwhile, it seems that I successfully tweaked GeographicLib to truncate the model, but I need more checking.
The current result truncated to the degree and order 12 is (x, y, z -> magnitude):
-4.84317693203254, -5.7718657901979, -4.33518106747648 -> 8.69278967782123
(I don't know how to obtain only the perturbations).
 

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
Fixed - not a memory allocation problem but updating the FS matrix wasn't indexed properly. My bad.

Again, the code has been updated in the above.

Output should now read;

Code:
0.0017603492
0.0018629822
-0.0109476821
Program ended with exit code: 0

I'll check against the GeographicLib results.

---------- Post added at 11:40 AM ---------- Previous post was at 11:29 AM ----------

If one adds in the standard newtonian gravity term, I get:

Code:
-4.8430808366
-5.7719928525
-4.3351577126

8.6928088598
Program ended with exit code: 0

Pretty darn close. What gravity model is GeographicLib running?
 

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
Fixed - not a memory allocation problem but updating the FS matrix wasn't indexed properly. My bad.

Again, the code has been updated in the above.

Output should now read;

Code:
0.0017603492
0.0018629822
-0.0109476821
Program ended with exit code: 0

0.0017603492
0.0018629822
-0.0109476821

Good! Thank you.

If one adds in the standard newtonian gravity term, I get:

Code:
-4.8430808366
-5.7719928525
-4.3351577126

8.6928088598
Program ended with exit code: 0

Pretty darn close.

Yes, but I expected an almost identical result; differences of 10-4 m/s2 can't be tolerated for this kind of models. Do you use the correct values for this model:
ModelRadius 6378137
ModelMass 3986004.418e8
?

EDIT: if I use the correct EGM96 parameters I get exactly the same GeographicLib result to degree and order 360; good work!


What gravity model is GeographicLib running?

EGM96 in a binary format:
https://geographiclib.sourceforge.io/html/gravity.html#gravityinst
 
Last edited:

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
I would really like to thank you not only for this thread, but also for all your interesting and valuable posts. Carry on! :thumbup:
 

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
For those following this thread, you will undoubtedly have seen some Mathematica, Python and C code for calculating the perturbation terms of a body's gravitational force. And it may well have seemed that the force calculations in these notes seemed to have been manufactured 'from thin air' and bearing little relation to the mathematics of earlier posts in this thread. This post aims to remedy that by setting out the underlying maths used. It will also serves as a 'for the record' document so that I don't entirely forget what I did and how I did it.

I'm going to break this 'for the record' statement into two parts. In the first part, I'll just focus on the algorithm for calculating the perturbations due to the 'J_n' terms - i.e., the kind of gravitational perturbations that Orbiter permits. In the second part, I'll work through the derivation of the force calculations used in the full perturbation model (e.g. the EGM96 model - as per the above posts) and show how that derivation relates to the C code that I posted.

So, let's begin:

The Orbiter gravitational perturbation model - i.e., J_n terms only
At the start of this thread, 'cristiapi' wanted analytical expressions for the J_4 and J_5 perturbation terms of the gravitational perturbation force expressed in cartesian (x-y-z) coordinates. Explicit analytics expressions were given for terms relating to J_2, J_3, J_4 and J_5 - but if you look at those expressions, they quickly become rather cumbersome as the perturbation order is increased. Going beyond n=5 yields analytical expressions that can only be described as hideous. Surely there must be a better way of calculating the perturbation terms rather than relying upon these cumbersome expansion terms?

And, yes, there is. As it turns out, we don't need the analytical expressions at all. For a specific cartesian point (x,y,z), we can calculate the perturbation force at that point using a recursion scheme - rather than relying upon an explicit analytical expression that describes the perturbation force at all spatial points.

And here is how it works...

If our gravitational model contains only the [imath]J_n[/imath] terms, we can write the gravitational potential, [imath]U[/imath] as:

[math]U(x,\,y,\,z) = -\frac{\mu}{r} + \sum_{n=2}^{N}J_n\,\frac{1}{r^{n+1}}\,P_n(\xi)[/math]
where:

[imath]\mu[/imath] is the gravitational parameter ('GM') for the gravitating body in question;

[imath]r = \sqrt(x^2 + y^2 + z^2)[/imath] is the radius of the cartesian point [imath](x,\,y,\,z)[/imath] from the centre of mass of the gravitating body;

[imath]N \ge 2[/imath] is the maximum order of the perturbation expansion;

[imath]J_n[/imath] is the [imath]n[/imath]-th order perturbation expansion coefficient. It is a given tabulated constant. (Note, though, that this not the normalised expansion coefficients [imath]\tilde{J}_n[/imath] quoted in Orbiter's configuration files - but we'll come back to this a bit later.);

[imath]P_n(\xi)[/imath] is the Legendre polynomial of order [imath]n[/imath]; and

[imath]\xi[/imath] is [imath]z / r[/imath] where the [imath]z[/imath]-axis of the cartesian [imath]x[/imath]-[imath]y[/imath]-[imath]z[/imath] reference frame is chose such that it points along the instantaneous rotation axis of the gravitating body.

To calculate the gravitational force vector from this expression, the general procedure is to calculate:

[math](g_x,\,g_y,\,g_z) = (-\frac{\partial\,U}{\partial x}, -\frac{\partial\,U}{\partial y}, -\frac{\partial\,U}{\partial z})[/math]
If we apply this rule to the first term in the expression for [imath]U(x,\,y,\,z)[/imath] - i.e., [imath]-\mu/r[/imath], we get the usual Keplerian gravitational force vector. If we apply the rule to the perturbation terms, then we have to calculate the partial derivatives of [imath]\Delta U_n = J_n\,\frac{1}{r^{n+1}}\,P_n(\xi)[/imath] with respect to [imath]x[/imath], [imath]y[/imath] and [imath]z[/imath]. If we remember that both [imath]r[/imath] and [imath]\xi[/imath] are themselves functions of [imath]x[/imath], [imath]y[/imath] and [imath]z[/imath], we get:

[math]\begin{aligned} -\frac{\partial\,\Delta U_n}{\partial x} &= J_n\,r^{-n-4} \left((n+1)\,x\,r \,P_n(\xi )+x\,z\,P_n'(\xi )\right) \\ -\frac{\partial\,\Delta U_n}{\partial y} &= J_n\,r^{-n-4} \left((n+1)\,y\,r \,P_n(\xi )+y\,z\,P_n'(\xi )\right) \\ -\frac{\partial\,\Delta U_n}{\partial z} &= J_n\,r^{-n-4} \left((n+1)\,r\,z \,P_n(\xi )-\left(x^2+y^2\right)\,P_n'(\xi )\right) \end{aligned}[/math]
And since we know [imath]x[/imath], [imath]y[/imath], [imath]z[/imath] and [imath]J_n[/imath] - and, hence, [imath]r[/imath] and [imath]\xi[/imath] - we know immediately evaluate everything in this expression except the Legendre Polynomial, [imath]P_n(\xi)[/imath], and its derivative, [imath]P_n'(\xi)[/imath]. Now, we could use explicit expressions for these functions, but for large [imath]n[/imath], this becomes very unwieldy. A better approach is to use the known recursion relations for the Legendre Polynomials - i.e.,

[math]P_n(\xi) = ((2\,n - 1)\,\xi\,P_{n-1}(\xi) + (1-n)\,P_{n-2}(\xi)) /n[/math]
So, if we know [imath]P_0(\xi)[/imath] and [imath]P_1(\xi)[/imath] we can then calculate [imath]P_2(\xi)[/imath]. And having calculated this, we can then calculate [imath]P_3(\xi)[/imath] and so on. And since we know that:

[math]P_0(\xi) = 1[/math]
and

[math]P_1(\xi) = \xi[/math]
we have all that we need to calculate the Legendre Polynomials to arbitrary order. Equally, we can calculate a similar recursion scheme for the derivatives of the Legendre polynomials. Simply by taking the derivative with respect to [MATH]\xi[/MATH], we get:

[math]P_n'(\xi) = ((2\,n - 1)\,P_{n-1}(\xi) + (2\,n - 1)\,\xi\,P_{n-1}'(\xi) + (1-n)\,P_{n-2}'(\xi)) /n[/math]
and again, we know the starting values for this recursion scheme, namely:

[math]P_0'(\xi) = 0[/math]
and

[math]P_1'(\xi) = 1[/math]
We can easily translate this recursion scheme into C code:

Code:
    r     = sqrt(x*x + y*y + z*z);
    xi    = z / r;
 
    P[1]  =  xi;
    P[0]  = 1.0;
    DP[1] = 1.0;
    DP[0] = 0.0;
 
    for (int n = 2; n <= nmax; n++) {
        P[n]  = (2*n - 1) * xi * P[n-1] + (1-n) * P[n-2]; P[n] /= n;
        DP[n] = (2*n - 1) * P[n-1] + (2*n - 1) * xi * DP[n-1] + (1-n) * DP[n-2]; DP[n] /= n;
    }

where the array 'P' is used to store the Legendre polynomial values up to and including order 'nmax'; and the array 'DP' is used to store the derivatives of the Legendre polynomials up to and including order 'nmax'. Of course, there are some array declarations and other 'boilerplate' code missing from this - but this contains the core of the recursion scheme.

So, now we are in a position to calculate the total gravitational perturbation force. Before we do so, we need to talk about the normalised perturbation coefficients, [imath]\tilde{J}_n[/imath]. In the above, we have used the unnormalised perturbation coefficients, [imath]J_n[/imath]. This is how Orbiter's config files present the coefficients and, indeed, this is how most people represent these coefficients. What is the connection between the two? Well, it's simply this:

[math]J_n = \tilde{J}_n\,\mu\,R^n[/math]
where [imath]\mu[/imath] is a gravitational parameter ('GM') used in the normalisation - usually just the gravitating body's GM value; and [imath]R[/imath] is a radius used to normalise the coefficients - usually the gravitating body's physical radius. In terms of the normalised perturbation coefficients, then, we get:

[math]\begin{aligned} -\frac{\partial\,\Delta U_n}{\partial x} &= \tilde{J}_n\,\frac{\mu}{r^4}\,(\frac{R}{r})^n \left((n+1)\,x\,r \,P_n(\xi )+x\,z\,P_n'(\xi )\right) \\ -\frac{\partial\,\Delta U_n}{\partial y} &= \tilde{J}_n\,\frac{\mu}{r^4}\,(\frac{R}{r})^n \left((n+1)\,y\,r \,P_n(\xi )+y\,z\,P_n'(\xi )\right) \\ -\frac{\partial\,\Delta U_n}{\partial z} &= \tilde{J}_n\,\frac{\mu}{r^4}\,(\frac{R}{r})^n \left((n+1)\,r\,z \,P_n(\xi )-\left(x^2+y^2\right)\,P_n'(\xi )\right) \end{aligned}[/math]
Ans since we have calculated the Legendre polynomials and their derivatives using our recursion scheme, all terms and factors in these expressions are known - so we can immediately write C code to calculate the perturbation contributions up to order max:

Code:
phi   = R / r;
k     = r * r; k *=k; k = mu / k;
q = phi;
    for (int n = 2; n <= nmax; n++) {
        q *= phi;
        F[n][0] = k * J[n] * q * ((n+1) * P[n] * x * r + DP[n] * x * z) ;
        F[n][1] = k * J[n] * q * ((n+1) * P[n] * y * r + DP[n] * y * z) ;
        F[n][2] = k * J[n] * q * ((n+1) * P[n] * z * r - DP[n] * (x * x + y * y)) ;
    }

Then, having, calculated these, all that we need to do is to sum the contributions:

Code:
f[0] = 0.0;
    f[1] = 0.0;
    f[2] = 0.0;
    for (int n = 0; n <= nmax; n++) {
        f[0] += F[n][0];
        f[1] += F[n][1];
        f[2] += F[n][2];

And aside from boilerplate code, that really is all there is to it. Simple, really.

The more general expansion case used to generate the code for EGM96 (as described earlier in this thread) proceeds in much the same way - although life is made a little more complicated because one must work with the fully-normalised associated Legendre polynomials rather than their simpler Legendre polynomial brethren - but the basic scheme for constructing the perturbation forces of various orders and degrees remains the same. In a later post in this thread, and for completeness, I'll outline the logic behind the construction of the C code.
 
Last edited:

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
The purpose of this post is to explain the derivation of the C code used to calculate the gravity perturbation in the 'EGM96' Earth Gravity Model. As such, it is mathematical 'backfill' and a 'for the record' contribution to this thread.

Part of this thread's earlier dialogue included:

Even if I don’t need that accuracy, is there any way to write the code to include at least the sectorial coefficients (degree = order)?
Yes, give me a few days to get the recursion scheme right and I can upgrade the code to a full gravity model

In this exchange, 'cristiapi' asked for an algorithm to calculate a more complete gravity model of the Earth that went beyond the relatively straightforward one used by Orbiter involving only the assorted 'J_n' contributions to the perturbed gravity field. I said yes, I thought that could be done and produced some C code which (after a little debugging) did as requested.

That code was based on the EGM96 model (i.e., a year 1996 version) which was the latest full dataset for a full, Earth gravity model that I could readily identify. There is a newer version of the same, EGM2008 (i.e., a year 2008 version) but at the time of writing this note I was unable to identify the model coefficients for that model. So, the ensuing C code that I developed was based on the earlier EGM96 model instead. The table of coefficients for the model can be found here:

Data file for the EGM96 gravity model

This first few lines of this data file look like:

Code:
   2   0 -0.484165371736E-03  0.000000000000E+00  0.35610635E-10  0.00000000E+00
   2   1 -0.186987635955E-09  0.119528012031E-08  0.10000000E-29  0.10000000E-29
   2   2  0.243914352398E-05 -0.140016683654E-05  0.53739154E-10  0.54353269E-10
   3   0  0.957254173792E-06  0.000000000000E+00  0.18094237E-10  0.00000000E+00
   3   1  0.202998882184E-05  0.248513158716E-06  0.13965165E-09  0.13645882E-09
   3   2  0.904627768605E-06 -0.619025944205E-06  0.10962329E-09  0.11182866E-09
   3   3  0.721072657057E-06  0.141435626958E-05  0.95156281E-10  0.93285090E-10

In total, the data file contains:

"The file contains fully-normalized, unitless spherical harmonic coefficients and their standard deviations, for a gravitational model complete from degree 2 order 0, to degree and order 360."

In total, there 65338 records records in the file. In each row, there are two values of interest (in the third and fourth columns), so the total number of model parameters is around 130,000. Compare this with the gravity model for the Earth used by Orbiter which uses just four perturbation terms ([imath]J_2[/imath], [imath]J_3[/imath], [imath]J_4[/imath] and [imath]J_5[/imath]).

The underlying perturbation series used in the EGM96 model is:

[math]\Delta U_(x,y,z) = -\sum_{n=2}^{360}\frac{\mu\,R^n}{r^{n+1}}\,\sum_{m=0}^{n}\left(C_{n,m}\,\cos(m\,\phi) + S_{n,m}\,\sin(m\,\phi) \right)\,\tilde{P}_{n,m}(\xi)[/math]
where:

[imath](x,\,y,\,z)[/imath] is the spatial point (in the ECEF reference frame) at which one wishes to calculate the gravitational perturbation such that the z-axis points along the Earth's instantaneous rotation axis; and the x-axis points towards the prime meridian in that frame.

[imath]r = \sqrt{x^2+y^2+z^2}[/imath]

[imath]\xi = z / r[/imath]

[imath]\phi = \tan^{-1}(y/x)[/imath] is the longitudinal angle in the equatorial reference frame

[imath]\mu[/imath] is the gravitational parameter (the product 'GM') used to normalise the perturbation coefficients

[imath]R[/imath] is the reference radius used to normalise the perturbation coefficients

[imath]C_{n,m}[/imath] and [imath]S_{n,m}[/imath] are the coefficients picked up from the EGM96 model data file such that [imath]n[/imath] is the value in the first column of the data file; [imath]m[/imath] is the value in the second column; [imath]C_{n,m}[/imath] is the value from the third column; and [imath]S_{n,m}[/imath] is the value from the fourth column.

[imath]\tilde{P}_{n,m}(\xi)[/imath] are the 'fully-normalised associated Legendre functions'. Details of these functions can be found at fully-normalised associated Legendre functions

As always, the perturbation force due to all of these non-spherical gravity terms is given by:

[imath](g_x,\, g_y,\, g_z) = (-\frac{\partial\,U}{\partial x}, -\frac{\partial\,U}{\partial y}, -\frac{\partial\,U}{\partial z})[/imath]

So, to calculate the perturbation contributions, we have to take the partial derivatives of:

[imath]r^{-n-1}\,\cos(m\,\phi)\,\tilde{P}_{n,m}(\xi)[/imath]

and

[imath]r^{-n-1}\,\sin(m\,\phi)\,\tilde{P}_{n,m}(\xi)[/imath]

with respect to [imath]x[/imath], [imath]y[/imath] and [imath]z[/imath].


Bearing in mind that [imath]r=\sqrt{x^2 + y^2 + z^2}[/imath], [imath]\xi=z/r[/imath]; and [imath]\phi = \tan^{-1}(y/x)[/imath], we find that the gravitational perturbation contributions from the potential term:

[math]\Delta UC_{n,m} = -C_{n,m}\,\frac{\mu\,R^n}{r^{n+1}}\,\cos(m\,\phi)\,\tilde{P}_{n,m}(\xi)[/math]
are:

[math]\begin{aligned} -\frac{\partial}{\partial\,x}\Delta UC_{n,m} &= -C_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\,\left(\left((1+n) \,x \,r \,\cos (m \,\phi )-m \,\left(\frac{r}{\rho }\right)^2 \,y \,r \,\sin (m \,\phi )\right) \tilde{P}[I]{n,m}(\xi ) + x \,z \,\cos (m \,\phi ) \, \tilde{P}[/I]{n,m}'(\xi)\right) \\ -\frac{\partial}{\partial\,y}\Delta UC_{n,m} &= -C_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\,\left(\left((1+n) \,y \,r \,\cos (m \,\phi )+m \,\left(\frac{r}{\rho }\right)^2 \,x \,r \,\sin (m \,\phi )\right) \tilde{P}[I]{n,m}(\xi ) + y \,z \,\cos (m \,\phi ) \,\tilde{P}[/I]{n,m}'(\xi )\right) \\ -\frac{\partial}{\partial\,z}\Delta UC_{n,m} &= -C_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\,\left(\left((1+n)\,z\,r\,\cos (m \,\phi ) \,\tilde{P}[I]{n,\,m}(\xi ) - \left(x^2+y^2\right)\, \cos (m\, \phi ) \,\tilde{P}[/I]{n,m}'(\xi )\right)\right) \end{aligned}[/math]
and the gravitational perturbation contributions from the potential term:

[math]\Delta US_{n,m} = -S_{n,m}\,\frac{\mu\,R^n}{r^{n+1}}\,\sin(m\,\phi)\,\tilde{P}_{n,m}(\xi)[/math]
are

[math]\begin{aligned} -\frac{\partial}{\partial\,x}\Delta US_{n,m} &= -S_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\left(\left((1+n) \,x \,r \,\sin (m \,\phi )+m \,\left(\frac{r}{\rho }\right)^2 \,y \,r \,\cos (m \,\phi )\right) \tilde{P}[I]{n,m}(\xi )+x \,z \,\sin (m \,\phi ) \,\tilde{P}[/I]{n,m}'(\xi )\right) \\ -\frac{\partial}{\partial\,y}\Delta US_{n,m} &= -S_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\left(\left((1+n) \,y \,r \,\sin (m \,\phi )-m \,\left(\frac{r}{\rho }\right)^2 \,x \,r \,\cos (m \,\phi )\right) \tilde{P}[I]{n,m}(\xi )+y \,z \,\sin (m \,\phi ) \,\tilde{P}[/I]{n,m}'(\xi )\right) \\ -\frac{\partial}{\partial\,z}\Delta US_{n,m} &= -S_{n,m}\frac{\mu}{r^4}(\frac{R}{r})^n\left(\left((1+n)\,z\,r\,\sin (m \,\phi ) \,\tilde{P}[I]{n,m}(\xi )-\left(x^2+y^2\right)\, \sin (m\, \phi ) \,\tilde{P}[/I]{n,m}'(\xi )\right)\right) \end{aligned}[/math]
where [imath]\rho^2 = x^2 + y^2[/imath].

Notwithstanding that these expressions are a bit of a mess, we can readily calculate most of the terms and factors on the right-hand side of each. However, as before fro the simpler ‘[imath]J_n[/imath] only' case, we have to calculate [imath]\tilde{P}[I]{n,\,m}(\xi)[/imath] and [imath]\tilde{P}'[/I]{n,\,m}(\xi)[/imath] for all [imath]2\le n \le N[/imath] and for all [imath]0\le m \le N[/imath] for some cutoff value of [imath]n[/imath], [imath]N[/imath]. And, as before, we rely upon recursion to allow us to do this. Since we are dealing with the 'fully-normalised associated Legendre functions', the recursion relation is different. But following fully-normalised associated Legendre functions, we can write the recursion relations as:

[math]\tilde{P}[I]{n,\,m}(\xi) = a_{n,m}\,\xi\,\tilde{P}[I]{n-1,m}(\xi) - b[/I]{n,m}\,\tilde{P}_{n-2,m}(\xi)[/math]
for any [imath]n > m[/imath]; and

[math]\tilde{P}[I]{n,n}(\xi) = \sqrt{\frac{2\,n+1}{2\,n}}\,u\,\tilde{P}[/I]{n-1,n-1}(\xi)[/math]
where

[math]u=\sqrt{1-\xi^2}[/math]
and where

[math]a_{n,\,m} = \sqrt{\frac{4\,n^2 - 1}{n^2 - m^2}}[/math]
for [imath]n>m[/imath]; and

[math]b_{n,m} = a_{n,m} / a_{n-1,m}[/math]
for [imath]n>m+1[/imath]; and

[math]b_{n,n-1} = 0[/math]
This recursion sequence is started by setting intial values as follows:

[math]\begin{aligned} \tilde{P}_{0,0} &= 1 \\ \tilde{P}_{1,0} &= \sqrt{3}\,\xi \\ \tilde{P}_{1,1} &= \sqrt{3}\,u \end{aligned}[/math]
And given the recursion relations from just these three starting values, all of the values of the associated Legendre functions follow. In addition, by differentiating these recursion relations with respect to [imath]\xi[/imath], we can establish the recursion relations for [imath]\tilde{P}'_{n,m}(\xi)[/imath] as well.

Building the code
We are now in a position to build the C routine to calculate the gravitational perturbations for the EGM9 gravity model. Since we are likely to make repeated calls to the gravity perturbation function, the first thing we want to do is to read the EGM96 perturbation parameter data file and store the results somewhere for easy access. Stripping out boilerplate code, we need to something like:

Code:
    file = fopen("-- path to EGM96 data file --", "r");
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m <= n; m++) {
            fscanf(file , "%i %i %lf %lf %lf %lf \n" , &i, &j, &C[n][m], &S[n][m], &dum1, &dum2);
        }
    }
    fclose(file);

This code reads the data file and stores the perturbation coefficients in two arrays, C and S, up to some maximum perturbation order 'max'.

Next, and rather than calculate the perturbation coefficients each time we calculate a gravity perturbation, we do a one-off calculation of those coefficients storing these values in two arrays, 'a' and 'b':

Code:
    for (int n = 1; n <= nmax; n++) {
        for (int m = 0; m < n; m++) {
            a[n][m] = sqrt((double)(4*n*n - 1) / (double)(n*n - m*m));
        }
    }
 
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m < n-1; m++) {
            b[n][m] = a[n][m] / a[n-1][m];
        }
        b[n][n-1] = 0.0;
    }

Now, when we want to calculate a gravity perturbation, we need to apply the recursion relations set out above to calculate [MATH]\tilde{P}[I]{n,m}(\xi)[/MATH] and [MATH]\tilde{P}'[/I]{n,m}(\xi)[/MATH] for all orders [MATH]2\le n \le \text{nmax}[/MATH and [MATH]0\le m\le n[/MATH]. The code for doing this is:

Code:
    r    = sqrt(x*x + y*y + z*z);
    t    = z / r;
    u    = sqrt(1 - t*t);

    P[0][0]  = 1.0;
    P[1][0]  = sqrt(3.0) * t;
    P[1][1]  = sqrt(3.0) * u;
 
    DP[0][0] = 0.0;
    DP[1][0] = sqrt(3.0);
    DP[1][1] =-sqrt(3.0) * t / u;
 
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m < n; m++) {
            P[n][m]  = a[n][m] * t * P[n-1][m] - b[n][m] * P[n-2][m];
            DP[n][m] = a[n][m] * P[n-1][m] + a[n][m] * t * DP[n-1][m] - b[n][m] * DP[n-2][m];
        }
        P[n][n]  = sqrt((double)(2*n + 1) / 2 / n) * u * P[n-1][n-1];
        DP[n][n] = sqrt((double)(2*n + 1) / 2 / n) * (u * DP[n-1][n-1] - t * P[n-1][n-1] / u);
    }

The values and derivatives of the fully-normalised associated Legendre functions are stored in arrays 'P' and 'DP' respectively.

In addition to this recursion, we can also calculate the [MATH]\cos(m\,\phi)[/MATH] and [MATH]\sin(m\,\phi)[/MATH] recursively. The code to this is:

Code:
    rho  = sqrt(x*x + y*y);

    cn[0] = 1.0;
    sn[0] = 0.0;
 
    for (int m = 1; m <= nmax; m++) {
        cn[m] = (x * cn[m-1] - y * sn[m-1]) / rho;
        sn[m] = (y * cn[m-1] + x * sn[m-1]) / rho;
    }

with the results of the recursion stored in the arrays 'cn' and 'sn'.

Now, finally, we can calculate the gravitational perturbation by calculating all of the sine and cosine terms to all relevant orders. This we do with the code snippet:

Code:
    double mu = 3986004.415e+8; //[M]^3[S]^-2
    double  R = 6378136.3;  //[M]

    k    = mu / r / r / r / r;
    phi  = R / r;
 
    q  = phi;
    for (int n = 2; n <= nmax; n++) {
        q *= phi;
        for (int m = 0; m <=n; m++) {
            FC[n][m][0] = -C[n][m]*k*q*(r*((n+1)*x*cn[m]-m*pow(r/rho, 2)*y*sn[m])*P[n][m]+x*z*cn[m]*DP[n][m]);
            FC[n][m][1] = -C[n][m]*k*q*(r*((n+1)*y*cn[m]+m*pow(r/rho, 2)*x*sn[m])*P[n][m]+y*z*cn[m]*DP[n][m]);
            FC[n][m][2] = -C[n][m]*k*q*(r* (n+1)*z*cn[m]*P[n][m]-(x*x + y*y)*cn[m]*DP[n][m]);
 
            FS[n][m][0] = -S[n][m]*k*q*(r*((n+1)*x*sn[m]+m*pow(r/rho, 2)*y*cn[m])*P[n][m]+x*z*sn[m]*DP[n][m]);
            FS[n][m][1] = -S[n][m]*k*q*(r*((n+1)*y*sn[m]-m*pow(r/rho, 2)*x*cn[m])*P[n][m]+y*z*sn[m]*DP[n][m]);
            FS[n][m][2] = -S[n][m]*k*q*(r* (n+1)*z*sn[m]*P[n][m]-(x*x + y*y)*sn[m]*DP[n][m]);
        }
    }

The results are stored in the arrays, 'FC' and 'FS'. Here 'mu' and 'R' are defined quantities in the EGM96 model and, for this gravity model we must use the listed values in our calculations (see EGM96 model description file)

And, then, we can calculate sum over all of these terms to give us the total gravitational perturbation:

Code:
    f[0] = 0;
    f[1] = 0;
    f[2] = 0;
    for (int n = 2; n <= nmax; n++) {
        for (int m = 0; m <= n; m++) {
            f[0] += FC[n][m][0]; f[0] += FS[n][m][0];
            f[1] += FC[n][m][1]; f[1] += FS[n][m][1];
            f[2] += FC[n][m][2]; f[2] += FS[n][m][2];
        }
    }

And that is basically all there is to it.
 
Last edited:

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
That code was based on the EGM96 model (i.e., a year 1996 version) which was the latest full dataset for a full, Earth gravity model that I could readily identify. There is a newer version of the same, EGM2008 (i.e., a year 2008 version) but at the time of writing this note I was unable to identify the model coefficients for that model.

Here you can find many models: http://icgem.gfz-potsdam.de/home
I'm currently using the first of the static models list: SGG-UGM-1 (truncated to the degree and order 15), because it is a combined model: EGM2008 + GOCE high accuracy data.

"3D Visualisation" could be very interesting.
 

MontBlanc2012

Active member
Joined
Aug 13, 2017
Messages
138
Reaction score
26
Points
28
Thanks for the link. It's useful to have a record of where the data files can be found.

A quick glance at the EGM2008 files suggests that aside from a slight change in format the coefficient normalisation is the same as for the EGM96 model parameter data file - so, the same core calculations can be used to calculate the gravitational perturbation in that model.

I'm not at all familiar with GOCE, but I think truncating at a relatively order of around 15 makes sense. Whereas normal Keplerian gravity falls off as [imath]r^{-2}[/imath], the higher order components of all of these gravity models fall of as [imath]r^{-n-2}[/imath] (where [imath]n[/imath] is the order number of the perturbation component.) So, even the very first perturbation term (e.g., [imath]J_2[/imath]) falls off as [imath]r^{-4}[/imath].

As an example of how this impacts the importance of various contributions, let's suppose that you want to model the behaviour of a satellite at an orbital radius of around 20,000 km, say (i.e., roughly the orbital radius of a GPS satellite). 20,000 km is roughly three times reference radius used to normalise the coefficients. This means that the [imath]n[/imath]=15 perturbation terms at 20,000 km are 3^-15 ~ 7e-8 times weaker than they are at the Earth's surface. In other words, they make a quite negligible contribution to the gravitational field at that distance. It is only if you consider satellites in Low Earth Orbit (a few hundred km in altitude) that it makes sense to include a large number of perturbation terms.

(Incidentally, this rapid fall off the strength of perturbation terms also explains why we ignore perturbation terms when using patched conic solutions to model interplanetary trajectories: the [imath]r^{-n-2}[/imath] fall off in contribution means that these perturbation terms are effectively zero unless we are very, very close to the planet.)
 
Last edited:

cristiapi

New member
Joined
May 26, 2014
Messages
222
Reaction score
0
Points
0
Location
Ancona
It is only if you consider satellites in Low Earth Orbit (a few hundred km in altitude) that it makes sense to include a large number of perturbation terms.

I do exactly the opposite. :)
I’m doing some experiments with very low orbit satellites (my main interest) and few high orbit satellites (like GPS).
When I include zonal, tesseral and sectorial harmonics, the result is usually better, but when the satellite flies below 420 km also a simplified gravity model (which includes only the lower degree harmonics) should be good (with the notable exception of the Tiangong-2 station which needs a “complete” model).
If a satellite orbits above 500 km (like HST), we need zonal, tesseral and sectorial harmonics; degree/order= 10 is not sufficient, degree/order= 25 is the same as 15 (with my propagator), then I use 15.

In other words, when the effect of the atmosphere is “big” we don’t need an accurate gravity model (at least not always).

Another “strange” but useful result is that when only zonal harmonics are used, it seems always better to use only low degree harmonics: J2 shall not be used alone, J2+J3 seems always better than J2+J3+J4+J5.
 
Last edited:
Top