Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Improper use of local frame rotation rate #553

Open
1 of 3 tasks
bcoconni opened this issue Dec 29, 2021 · 27 comments
Open
1 of 3 tasks

Improper use of local frame rotation rate #553

bcoconni opened this issue Dec 29, 2021 · 27 comments
Labels

Comments

@bcoconni
Copy link
Member

I'm submitting a ...

  • bug report
  • feature request
  • support request => Please do not submit support request here, see note at the top of this template.

Describe the issue

JSBSim computes the rotation rate of the local frame in src/initialization/FGInitialCondition.cpp:

// Refer to Stevens and Lewis, 1.5-14a, pg. 49.
// This is the rotation rate of the "Local" frame, expressed in the local frame.
const FGMatrix33& Tl2b = orientation.GetT();
double radInv = 1.0 / position.GetRadius();
FGColumnVector3 vOmegaLocal = {radInv*vUVW_NED(eEast),
-radInv*vUVW_NED(eNorth),
-radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};
vPQR_body = Tl2b * vOmegaLocal;

Problem: the formula from Stevens & Lewis uses the tangent of the latitude which obviously triggers math issues when the latitude is initialized to 90 degrees.

What is the current behavior?

When the initial latitude is modified to 90 degrees in aircraft/ball/reset00.xml

--- a/aircraft/ball/reset00.xml
+++ b/aircraft/ball/reset00.xml
@@ -7,7 +7,7 @@
     1584.2593825 + 23869.9759596 = 25454.235342 ft/sec
   -->
   <ubody unit="FT/SEC"> 23869.9759596 </ubody> 
-  <latitude unit="DEG">     0.0  </latitude>
+  <latitude unit="DEG">    90.0  </latitude>
   <longitude unit="DEG">    0.0  </longitude>
   <psi unit="DEG">         90.0  </psi>

then the tests CheckScripts and TestAccelerometer fail:

FAIL: testScripts (__main__.CheckScripts)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/bertrand/OpenSource/JSBSim/GitHub/JSBSim/tests/CheckScripts.py", line 39, in testScripts
    ExecuteUntil(fdm, 30.)
fpectl.FloatingPointError: Caught signal SIGFPE in JSBSim

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/bertrand/OpenSource/JSBSim/GitHub/JSBSim/tests/CheckScripts.py", line 43, in testScripts
    self.fail("Script %s failed:\n%s" % (s, e.args[0]))
AssertionError: Script ../../../../scripts/ball.xml failed:
Caught signal SIGFPE in JSBSim

----------------------------------------------------------------------

What is the expected behavior?

The tests should run successfully. There is no reason JSBSim should fail when the latitude initialized to the legal value of 90 degrees.

What is the motivation / use case for changing the behavior?

A review of the version control history shows the following facts:

  • Commit a3819b9 introduced the variable vOmegaLocal as the rotation rate of the local frame which was added to VState.vPQR

// Compute the local frame ECEF velocity
vVel = Tb2l*VState.vUVW;
vOmegaLocal.InitMatrix( radInv*vVel(eEast),
-radInv*vVel(eNorth),
-radInv*vVel(eEast)*VState.vLocation.GetTanLatitude() );
// Set the angular velocities of the body frame relative to the ECEF frame,
// expressed in the body frame.
VState.vPQR = FGColumnVector3( FGIC->GetPRadpsIC(),
FGIC->GetQRadpsIC(),
FGIC->GetRRadpsIC() ) + Tl2b*vOmegaLocal;

  • Commit 207cd7a moved the computation and usage of vOmegaLocal from src/models/FGPropagate.Cpp to src/initialization/FGInitialConditions.cpp

However VState.vPQR is measuring the angular velocity of the vehicle relative to the ECEF frame:

/** The angular velocity vector for the vehicle relative to the ECEF frame,
expressed in the body frame.
units rad/sec */
FGColumnVector3 vPQR;

So I am challenging the relevance of adding vOmegaLocal (rotation rate of the local frame with respect to the ECEF) to vPQR(angular velocity of the body frame with respect to the ECEF). Said otherwise, I think the vector vOmegaLocal should be removed altogether from JSBSim.

Other information

My point is that the formula 1.5-14a from Stevens & Lewis is relevant if the inertial angular velocity is split in 3 terms: w_b/i = w_b/v + w_v/e + w_e/i (the variable vOmegaLocal being w_v/eand in.OmegaPlanet is w_e/i in JSBSim parlance):

Steven & Lewis p48

Steven & Lewis p49

However, in JSBSim the inertial angular velocity is split in 2 terms: w_b/i = w_b/e + w_e/i where VState.vPWRi is w_b/i, VState.vPQR is w_b/e and in.OmegaPlanet is w_e/i. Said otherwise, there is no need for the term w_v/e i.e. vOmegaLocal.

@bcoconni bcoconni added the bug label Dec 29, 2021
@bcoconni
Copy link
Member Author

For the sake of completeness there is a second occurrence of vOmegaLocal in src/initialization/FGInitialCondition.cpp:

// Refer to Stevens and Lewis, 1.5-14a, pg. 49.
// This is the rotation rate of the "Local" frame, expressed in the local frame.
double radInv = 1.0 / position.GetRadius();
FGColumnVector3 vOmegaLocal = { radInv*vUVW_NED(eEast),
-radInv*vUVW_NED(eNorth),
-radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};

@bcoconni
Copy link
Member Author

The bug can be fixed by forcing vOmegaLocal to a null vector:

--- a/src/initialization/FGInitialCondition.cpp
+++ b/src/initialization/FGInitialCondition.cpp
@@ -1191,10 +1191,7 @@ bool FGInitialCondition::Load_v1(Element* document)
   // Refer to Stevens and Lewis, 1.5-14a, pg. 49.
   // This is the rotation rate of the "Local" frame, expressed in the local frame.
   const FGMatrix33& Tl2b = orientation.GetT();
-  double radInv = 1.0 / position.GetRadius();
-  FGColumnVector3 vOmegaLocal = {radInv*vUVW_NED(eEast),
-                                 -radInv*vUVW_NED(eNorth),
-                                 -radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};
+  FGColumnVector3 vOmegaLocal = { 0., 0., 0.,};
 
   vPQR_body = Tl2b * vOmegaLocal;
 
@@ -1413,10 +1410,7 @@ bool FGInitialCondition::Load_v2(Element* document)
 
   // Refer to Stevens and Lewis, 1.5-14a, pg. 49.
   // This is the rotation rate of the "Local" frame, expressed in the local frame.
-  double radInv = 1.0 / position.GetRadius();
-  FGColumnVector3 vOmegaLocal = { radInv*vUVW_NED(eEast),
-                                  -radInv*vUVW_NED(eNorth),
-                                  -radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};
+  FGColumnVector3 vOmegaLocal = { 0., 0., 0. };
 
   if (attrate_el) {

@seanmcleod, @jonsberndt, @agodemar I'm waiting for your feedback before removing vOmegaLocal as described above.

@bcoconni
Copy link
Member Author

The formula -radInv*vUVW_NED(eEast)*tan(position.GetLatitude() is really a big issue when the latitude is close to 90 degrees even for small velocities: it can initialize the vehicle with a huge spin velocity around the local Z (up/down) axis. Therefore we must get rid of this formula one way or another.

@seanmcleod
Copy link
Member

seanmcleod commented Dec 30, 2021

My point is that the formula 1.5-14a from Stevens & Lewis is relevant if the inertial angular velocity is split in 3 terms: w_b/i = w_b/v + w_v/e + w_e/i (the variable vOmegaLocal being w_v/e and in.OmegaPlanet is w_e/i in JSBSim parlance):

In trying to get up to speed I've been reading my copy of Stevens & Lewis which happens to be the 3rd edition, I'm guessing you're quoting from the 2nd or 1st edition? What I noticed is that in the 3rd edition the angular velocity isn't split into 3 terms.

So in the 3rd edition's equation 1.7-16a w_b/e = w_b/v + w_v/e in 2nd edition equation 1.5-13?

And the v in w_b/v and w_v/e refers to the local NED frame right?

image

@bcoconni
Copy link
Member Author

In trying to get up to speed I've been reading my copy of Stevens & Lewis which happens to be the 3rd edition, I'm guessing you're quoting from the 2nd or 1st edition? What I noticed is that in the 3rd edition the angular velocity isn't split into 3 terms.

Yes I'm quoting the 2nd edition because it's the only edition that I could find on the internet as I don't have an access to my own personal copy (3rd edition as well) at the moment.

So in the 3rd edition's equation 1.7-16a w_b/e = w_b/v + w_v/e in 2nd edition equation 1.5-13?

That's my understanding as well.

And the v in w_b/v and w_v/e refers to the local NED frame right?

Correct.

@jonsberndt
Copy link
Contributor

This is only a problem at a boundary case, right?

Just checking. The EOM has been verified against a few NASA simulations (several years ago), so I know that generally speaking the EOM is solid.

@bcoconni
Copy link
Member Author

This is only a problem at a boundary case, right?

Sort of.

Just checking. The EOM has been verified against a few NASA simulations (several years ago), so I know that generally speaking the EOM is solid.

This problem is not involving the EOM integration but the initialization of VState.vPQR. The thing is that vOmegaLocal component values are extremely small in most cases which makes its contribution negligible. The best evidence of its irrelevance is that setting vOmegaLocal to the null vector makes no noticeable difference to the computations made by JSBSim (all our test succeeds including the test that compares against the reference jsbsim/check_cases/orbit/logged_data/BallOut.csv, a file that has not been modified for the last 6 years) as long as the initial location is far enough away from the Poles. On the other hand, the Z component of vOmegaLocal contains huge values at the North Pole which leads to NaN values.

The sad thing is that vOmegaLocal (which contains the rotation rate of the local frame wrt ECEF) should not be used to initialize the angular velocity VState.vPQR since the latter contains a different quantity: the rotation rate of the body frame wrt ECEF.

@seanmcleod
Copy link
Member

So in summary, looking at the current initialization code.

So if we're dealing with a V1 initialization file then angular rates for the aircraft can't be specified by the user.

void FGPropagate::SetInitialState(const FGInitialCondition* FGIC)
{
...
  // Set the angular velocities of the body frame relative to the ECEF frame,
  // expressed in the body frame.
  VState.vPQR = FGIC->GetPQRRadpsIC();

  VState.vPQRi = VState.vPQR + Ti2b * in.vOmegaPlanet;   
}

  /** Gets the initial body rotation rate
      @return Initial body rotation rate in radians/second */
  const FGColumnVector3 GetPQRRadpsIC(void) const { return vPQR_body; }

bool FGInitialCondition::Load_v1(Element* document)
{
...
  vPQR_body = Tl2b * vOmegaLocal;
}

bool FGInitialCondition::Load_v2(Element* document)
{
...
  if (attrate_el) {

    string frame = attrate_el->GetAttributeValue("frame");
    frame = to_lower(frame);
    FGColumnVector3 vAttRate = attrate_el->FindElementTripletConvertTo("RAD/SEC");

    if (frame == "eci") {
      FGMatrix33 Ti2l = position.GetTec2l() * Ti2ec;
      vPQR_body = Tl2b * Ti2l * (vAttRate - vOmegaEarth);
    } else if (frame == "ecef") {
      vPQR_body = Tl2b * position.GetTec2l() * vAttRate;
    } else if (frame == "local") {
      vPQR_body = Tl2b * (vAttRate + vOmegaLocal);
    } else if (frame == "body") {
      vPQR_body = vAttRate;
    } else if (!frame.empty()) { // misspelling of frame

      cerr << endl << fgred << "  Attitude rate frame type: \"" << frame
           << "\" is not supported!" << reset << endl << endl;
      result = false;

    } else if (frame.empty()) {
      vPQR_body = Tl2b * vOmegaLocal;
    }

  } else { // Body frame attitude rate assumed 0 relative to local.
      vPQR_body = Tl2b * vOmegaLocal;
  }

}

The sad thing is that vOmegaLocal (which contains the rotation rate of the local frame wrt ECEF) should not be used to initialize the angular velocity VState.vPQR since the latter contains a different quantity: the rotation rate of the body frame wrt ECEF.

So given the current code in JSBSim listed above in terms of initialization I'm not so sure that vOmegaLocal is being used incorrectly. Excluding the issue with tan(lat) as it approaches the poles.

vPQR_body = w_b/e

vOmegaLocal = w_v/e

Snippets from original JSBSim Reference Manual PDF.

image

image

So taking the Load_v1() case the user can't specify any angular rates for the aircraft, but they can specify translational velocities. If all the translational velocities are all 0 then vOmegaLocal will be 0, and if they aren't then they will result in angular velocities relative to the ECEF frame as captured by vOmegaLocal so the initialization of vPQR via vOmegaLocal is expected right?

@seanmcleod
Copy link
Member

@bcoconni @jonsberndt any further thoughts?

@bcoconni
Copy link
Member Author

Oh yes , many thoughts but I need to put my ideas in writing. You made a good point but as far as I'm concerned that's not the end of the story 🙂 Following your comment I'd like to change the title of this issue to Improper use of local frame rotation rate. Hopefully I'll be able to contribute something later this week.

@bcoconni
Copy link
Member Author

bcoconni commented Jan 15, 2022

First of all, I'd like to stress that the current issue is not the result of a formula that we are trying to use outside of its domain of validity. The formula 1.5-14a from Stevens & Lewis 2nd edition is an exact formula that is giving a correct result for all latitudes from -90 degrees to +90 degrees. However...

Analogy with the gimbal lock problem for Euler angles

What we are facing here is an issue similar to the gimbal lock for Euler angles. The conversion from matrix transformations to the Euler angles is using exact formulas, still those are giving NaNs when the pitch angle is +/-90 degrees. These NaNs are not due to an error in the formulas but they are due to a mathematical ambiguity between roll and yaw angles in that position.

This has been worked around in JSBSim by forcing the yaw angle to zero (which is an arbitrary choice) when the gimbal is, well, locked:

if (GimbalLock)
mEulerAngles(3) = 0.0;
else {
double psi = atan2(data[3], data[0]);
if (psi < 0.0)
psi += 2*M_PI;
mEulerAngles(3) = psi;
}

As a consequence the Euler angles rates are undefined for the gimbal lock situation.

I also would like to continue this analogy to address @jonsberndt's comment:

This is only a problem at a boundary case, right?

The issue with the local frame rotation rate is just as much a boundary case than the gimbal lock is a boundary case for the Euler angles. This does not allow us to dismiss the problem since we don't want JSBSim to crash abruptly or give silly results when the aircraft nose is pointing to the sky, do we ? Even so if the likeliness of flying straight to the sky at an exact pitch of +90 degrees is tenuous.

Thus I see no reason why the current issue (undefined local frame rotational rates at the Poles) could be dismissed because it is very unlikely that someone would fly very close to the Poles. We are aiming at a robust FDM software that behaves well even in corner cases, aren't we ?

Initializing vPQR to the local frame rotation rate is meaningless

Let's consider a vehicle initially located at a point A and travelling with a constant eastern velocity V0 (see the figure below). At this initial point A, the local frame orientation is LA and JSBSim initializes by default the vehicle rotational rate vPQR to the local frame rotational rate vOmegaLocal.

Vehicle longitude

After some time, the vehicle will be located in B and the local frame orientation will be LB. Since the vehicle rotational rate vPQR has been initialized to the local frame rotational rate, this means that when the vehicle has reached the point B its orientation will be almost identical to the local frame orientation LB. But why would that be so ? Why a vehicle travelling at a constant velocity V0 in a straight line from a point A to a point B should have its orientation modified at all ? Well, there is no reason. Initializing vPQR to the local frame rotational rate vOmegaLocal is meaningless.

Things are going bad in the vicinity of the Poles

When the vehicle is travelling from A to B, by inspection of the figure above, we find that the variation of longitude is

$$\tan(\Delta\ell)=\frac{AB}{R_A}$$

where RA is the distance of A to the Earth rotation axis. And since the vehicle is travelling at a constant speed V0 then AB=V0*Δt and the variation of longitude is therefore

$$\tan(\Delta\ell)=\frac{V_0\Delta t}{R_A}$$

Taking the limit as Δt tends to 0, we get the longitudinal rate (as Δt tends to 0, tan(Δl) is almost equal to Δl)

$$\dot{\ell}=\lim_{\Delta t\to 0} \frac{\tan(\Delta\ell)}{\Delta t}=\frac{V_0}{R_A}$$

It is therefore obvious that for a given constant velocity V0, as RA is getting lower then the longitudinal rate (l dot) is increasing. Said otherwise, for any finite velocity V0, the longitudinal rate will diverge (ultimately to the infinity) as the vehicle is getting closer to the Poles (i.e. closer to the Earth axis rotation).

Using the formula for vOmegaLocal (i.e. w_v/e) from Stevens & Lewis

Stevens & Lewis formula

we see that if the longitudinal rate (l dot) goes to infinity, then so will w_v/e (i.e. vOmegaLocal).

As I explained in the introduction of this comment, there is no approximation involved there. As the vehicle is travelling closer to the Poles, the radius of the circle of latitude on which the local frame is constrained to move will get smaller and the longitudinal rate (l dot) will get larger even for a moderate velocity V0. Ultimately vOmegaLocal diverges to infinity when the vehicle is exactly over the Poles.

The reason is that when the vehicle is just over the Poles, East and North directions are undefined and as a consequence the orientation of the local frame is undefined: it is then impossible to evaluate the longitudinal rate (l dot). This is exactly the same situation than the gimbal lock for the Euler angles. The divergence is the result of the construction of the local frame: it is impossible to compute the longitudinal rate (l dot) at the Poles.

Way forward

The local frame rotation rate is diverging when getting close to the Poles, there is no way around that. But the good news is that there is no reason why we should force the vehicle to rotate at the same rate than the local frame. So the obvious change to fix that issue is to no longer use vOmegaLocal to initialize vPQR.

With one exception: when the vehicle rotation rate is explicitly initialized in the local frame (thanks @seanmcleod for making me realize that exception).

} else if (frame == "local") {
vPQR_body = Tl2b * (vAttRate + vOmegaLocal);
} else if (frame == "body") {

@bcoconni bcoconni changed the title Outdated use of local frame rotation rate Improper use of local frame rotation rate Jan 15, 2022
@seanmcleod
Copy link
Member

This is only a problem at a boundary case, right?

This does not allow us to dismiss the problem since we don't want JSBSim to crash abruptly

I can't say for sure what @jonsberndt's thinking is, but my guess is he was thinking that it could potentially be solved along the lines of the gimbal lock, i.e. boundary check for a latitude of +-90 degrees and do something in this case to avoid tan blowing up etc., as opposed to simply ignoring the crash.

@bcoconni
Copy link
Member Author

I can't say for sure what @jonsberndt's thinking is, but my guess is he was thinking that it could potentially be solved along the lines of the gimbal lock, i.e. boundary check for a latitude of +-90 degrees and do something in this case to avoid tan blowing up etc., as opposed to simply ignoring the crash.

The problem is that you can "do something" about undefined angles (either Euler or North/East), for instance by forcing their value to an arbitrary value. By undefined value it is meant that an infinity of values are a solution to the problem. So you pick one value among the infinite many possible solutions and bam! the problem vanishes.

However you can't do anything about their rate because determining a function derivative at an undefined value is undecidable. That's the very reason why their derivatives diverge to infinity.

Even Euler angles rates can't be computed at the gimbal lock so JSBSim is just keeping the previous rates unchanged i.e. using their previous finite value whatever that was:

if (in.CosTht != 0.0) {
vEulerRates(ePsi) = (in.vPQR(eQ)*in.SinPhi + in.vPQR(eR)*in.CosPhi)/in.CosTht;
vEulerRates(ePhi) = in.vPQR(eP) + vEulerRates(ePsi)*in.SinTht;
}

However this strategy somehow works because JSBSim does not do anything from the Euler rates. They are just computed for users to do whatever they see fit from these parameters. But the situation is much different for vOmegaLocal: it is used in almost cases when the vehicle rotational rates are initialized.

What I don't understand is why vOmegaLocal must be kept at all cost to initialize vPQR ? Why using vOmegaLocal is so vital that we must keep it that way and make up a work around for the cases where it initializes vPQR to huge meaningless values ? Removing vOmegaLocal is simple enough, isn't it ?

I have built a small example that shows that the current code forces a vehicle to spin in the absence of forces and moments (since we don't need to make any assumption about forces and moments to obtain that result). I'd be curious to know why that is the expected and correct behaviour ?

@jonsberndt
Copy link
Contributor

jonsberndt commented Jan 16, 2022 via email

@seanmcleod
Copy link
Member

I'm definitely no expert in terms of the equations of motion and their implementation with respect to all the relevant reference frames, so please double-check my comments 😉

First off, are we not missing w_b/v? See earlier snippet I posted from JSBSim manual.

So we have PQR == w_b/e and PQRi == w_b/i, but no w_b/v?

  PropertyManager->Tie("velocities/p-rad_sec", this, eP, (PMF)&FGPropagate::GetPQR);
  PropertyManager->Tie("velocities/q-rad_sec", this, eQ, (PMF)&FGPropagate::GetPQR);
  PropertyManager->Tie("velocities/r-rad_sec", this, eR, (PMF)&FGPropagate::GetPQR);

  PropertyManager->Tie("velocities/pi-rad_sec", this, eP, (PMF)&FGPropagate::GetPQRi);
  PropertyManager->Tie("velocities/qi-rad_sec", this, eQ, (PMF)&FGPropagate::GetPQRi);
  PropertyManager->Tie("velocities/ri-rad_sec", this, eR, (PMF)&FGPropagate::GetPQRi);

So if an aircraft is running down a runway due east with some velocity, then w_b/v would be 0 right? But w_b/e, i.e. relative to ECEF wouldn't be 0 right?

The point with vOmegaLocal is to initialize w_b/e in this case.

Glancing at the JSBSim source it looks like the actual integration and propagation takes place in the ECI frame. So w_b/e and w_b/v would then be updated from the ECI frame I assume and which should still result in w_b/v being 0?

@seanmcleod
Copy link
Member

What I don't understand is why vOmegaLocal must be kept at all cost to initialize vPQR ?

I'm not advocating for it be kept at all costs, I'm still getting up to speed on the issues and options 😉

Thus I see no reason why the current issue (undefined local frame rotational rates at the Poles) could be dismissed because it is very unlikely that someone would fly very close to the Poles. We are aiming at a robust FDM software that behaves well even in corner cases, aren't we ?

I agree JSBSim should behave well even in corner cases. Although we could make this corner case a smaller corner than 'fly very close to the Poles'.

It's only an issue with initialization as you mentioned, so if the aircraft was initialized away from the Pole it could still fly over/close to the Pole. Plus it's only an issue if the initialization is at or close to the Poles and some initial translational velocity is specified. In other words the case of an aircraft starting it's take-off at the Pole with an initial 0 velocity would be fine if we modified the following code to check if vUVW_NED was 0 before calculating vOmegaLocal.

 double radInv = 1.0 / position.GetRadius(); 
 FGColumnVector3 vOmegaLocal = { radInv*vUVW_NED(eEast), 
                                 -radInv*vUVW_NED(eNorth), 
                                 -radInv*vUVW_NED(eEast)*tan(position.GetLatitude())}; 

@bcoconni
Copy link
Member Author

So if an aircraft is running down a runway due east with some velocity, then w_b/v would be 0 right? But w_b/e, i.e. relative to ECEF wouldn't be 0 right?

There is a strong assumption behind this statement: you are assuming that the runway follows the Earth curvature. Indeed in that case, w_b/v would be 0.

Now if we assume the same situation (a runway due East with a non zero velocity) but this time we assume that the runway has a slope then your assumption breaks down since w_b/v can be any value even equal to -w_v/e if the runway is flat and tangent to the Earth curvature (see figure below).

Runway

Of course the picture above is grossly exaggerated but the point is that you cannot assume that the aircraft is following the Earth curvature when running on a runway.

Now let's assume the aircraft is initialized with some eastward velocity on the runway shown above then w_b/e will be zero at any time so setting w_b/e == w/v_e is incorrect since w_b/e must be zero at any time.

@bcoconni
Copy link
Member Author

bcoconni commented Jan 16, 2022

What I don't understand is why vOmegaLocal must be kept at all cost to initialize vPQR ?

I'm not advocating for it be kept at all costs, I'm still getting up to speed on the issues and options 😉

Sorry, I have overreacted there but I'd really like the discussion to focus on the reason why we are using vOmegaLocal and whether it is correct or not. I have this impression that when we are discussing about finding a work around then we are implicitly making the assumption that there is no questioning about the correctness of using vOmegaLocal to initialize vPQR.

At the moment, I am not convinced at all that initializing vPQR to vOmegaLocal is the way to proceed so I am focusing on that point. If the discussion reaches the conclusion that vOmegaLocal should be used then I'll gladly discuss about finding a way around the divergence of the Stevens & Lewis formula. But we are not there yet 😉

I agree JSBSim should behave well even in corner cases. Although we could make this corner case a smaller corner than 'fly very close to the Poles'.

In fact, it does not have to be a corner case at all since in my opinion it is the result of an incorrect initialization.

It's only an issue with initialization as you mentioned, so if the aircraft was initialized away from the Pole it could still fly over/close to the Pole.

It's also an issue when a satellite is initialized with a polar orbit. In that case, the easiest way to initialize the satellite is to spawn it over the Poles with a Z velocity component set to zero in the ECEF frame rather than spawning it elsewhere on the orbit and aiming at the Poles.

Plus it's only an issue if the initialization is at or close to the Poles and some initial translational velocity is specified. In other words the case of an aircraft starting it's take-off at the Pole with an initial 0 velocity would be fine if we modified the following code to check if vUVW_NED was 0 before calculating vOmegaLocal.

I am not sure I am following your reasoning here, since if we only use the formula when vUVW_NED is 0 then vOmegaLocal is trivially zero as well.

@bcoconni
Copy link
Member Author

It's also an issue when a satellite is initialized with a polar orbit. In that case, the easiest way to initialize the satellite is to spawn it over the Poles with a Z velocity component set to zero in the ECEF frame rather than spawning it elsewhere on the orbit and aiming at the Poles.

This example made me realize that using vOmegaLocal is meaningless because the initial rotational rate vPQR of a satellite on a polar orbit would depend on the position at which it is initialized.

Say the satellite is initialized very close to the Pole (not exactly at the Pole because East would be meaningless) with an Eastward velocity then vOmegaLocal(eZ) will be huge non zero meaning that the satellite will spin around its vertical axis (Up/Down axis in the local frame Z axis).

Now if the same satellite is initialized on the same polar orbit but just above the equator then its Eastward velocity will be zero meaning that vOmegaLocal(eX) and vOmegaLocal(eZ) will be zero. The satellite will not spin at all around its vertical axis (Up/Down axis in the local frame Z axis).

So the initial value of vPQR for this satellite will depend on the position at which it will be spawned. Weird, isn't it ?

@seanmcleod
Copy link
Member

Sorry, I have overreacted there but I'd really like the discussion to focus on the reason why we are using vOmegaLocal and whether it is correct or not.

No problem. But I was making the point/asking whether the example of an aircraft with an initial translational velocity doesn't imply that vOmegaLocal needs to be used to correctly set the initial value of w_b/e.

If all the translational velocities are all 0 then vOmegaLocal will be 0, and if they aren't then they will result in angular velocities relative to the ECEF frame as captured by vOmegaLocal so the initialization of vPQR via vOmegaLocal is expected right?

But I can see now, based on your most recent replies that there is an underlying assumption that these NED velocities are following the curvature of the earth, if they were perfectly tangential like in your recent runway picture then that would mean they wouldn't generate a rotation around the ECEF.

So I'm leaning towards your point that vOmegaLocal shouldn't be used. Let's see what @jonsberndt thoughts are.

I am not sure I am following your reasoning here, since if we only use the formula when vUVW_NED is 0 then vOmegaLocal is trivially zero as well.

The point I was trying to make was that currently the code throws an exception from tan(90) when calculating vOmegaLocal even in the case where vUVW_NED is 0. So what I was suggesting is that we could check if vUVW_NED was 0 and if it was skip calculating vOmegaLocal to avoid the exception.

But if you as you're suggesting we get rid of vOmegaLocal then that's a moot suggestion.

@bcoconni
Copy link
Member Author

So I'm leaning towards your point that vOmegaLocal shouldn't be used. Let's see what @jonsberndt thoughts are.

Thanks. Let's wait for @jonsberndt opinion.

So the initial value of vPQR for this satellite will depend on the position at which it will be spawned. Weird, isn't it ?

Below is a diff to tests/TestInitialConditions.py that checks that condition. Unsurprisingly, the test fails.

======================================================================
FAIL: testIndependenceOfInitialLocation (__main__.TestInitialConditions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/bertrand/OpenSource/JSBSim/GitHub/JSBSim/tests/TestInitialConditions.py", line 413, in testIndependenceOfInitialLocation
    self.assertAlmostEqual(fdm['ic/r-rad_sec'], r)
AssertionError: 0.0 != -0.6315477934082796 within 7 places (0.6315477934082796 difference)

----------------------------------------------------------------------

Basically, the test spawns the satellite very close to the North Pole (at a latitude of 89.9 degrees i.e. 35000 ft away on the Earth surface from the Pole) with an eastward velocity. The initial values of the vPQR components are stored in the variables p, q and r. Then the test spawns the satellite above the equator heading North at the same radius and the same velocity.

As the failure message above shows, the r component is then significantly different for between the 2 initial conditions (and there is no reason for this to happen: the vehicle is describing the same orbit in both cases). For the record, -0.631 rad/s is equal to 36 deg/s or 6 rpm, far from being zero and still the satellite was initialized 35000 ft away from the Pole. And results are getting worst as the vehicle is initialized closer to Pole.

diff --git a/tests/TestInitialConditions.py b/tests/TestInitialConditions.py
index 12710cf9..2baf1053 100644
--- a/tests/TestInitialConditions.py
+++ b/tests/TestInitialConditions.py
@@ -23,7 +23,9 @@
 import os, math, shutil
 import xml.etree.ElementTree as et
 import pandas as pd
-from JSBSim_utils import append_xml, ExecuteUntil, JSBSimTestCase, RunTest, CreateFDM
+from JSBSim_utils import append_xml, ExecuteUntil, JSBSimTestCase, RunTest, CopyAircraftDef
 
 # Values copied from FGJSBBase.cpp and FGXMLElement.cpp
 convtoft = {'FT': 1.0, 'M': 3.2808399, 'IN': 1.0/12.0}
@@ -45,7 +47,7 @@ class TestInitialConditions(JSBSimTestCase):
         IC_file = append_xml(use_tag.attrib['initialize'])
         IC_tree = et.parse(os.path.join(path_to_jsbsim_aircrafts, IC_file))
 
-        return (tree, IC_tree)
+        return tree, IC_tree, IC_file
 
     def test_initial_conditions_v1(self):
         prop_output_to_CSV = ['velocities/vc-kts']
@@ -107,7 +109,7 @@ class TestInitialConditions(JSBSimTestCase):
 
         for s in self.script_list(('ZLT-NT-moored-1.xml',
                                    '737_cruise_steady_turn_simplex.xml')):
-            (tree, IC_tree) = self.getElementTrees(s)
+            tree, IC_tree, _ = self.getElementTrees(s)
             IC_root = IC_tree.getroot()
 
             # Only testing version 1.0 of init files
@@ -257,7 +259,7 @@ class TestInitialConditions(JSBSimTestCase):
     def test_geod_position_from_init_file_v2(self):
         for s in self.script_list(('ZLT-NT-moored-1.xml',
                                    '737_cruise_steady_turn_simplex.xml')):
-            (tree, IC_tree) = self.getElementTrees(s)
+            tree, IC_tree, _ = self.getElementTrees(s)
             IC_root = IC_tree.getroot()
 
             # Only testing version 2.0 of init files
@@ -355,10 +357,59 @@ class TestInitialConditions(JSBSimTestCase):
     # Regression test for the bug reported by @fernandafenelon and fixed by
     # Sean McLeod in #545.
     def testClimbRateSetting(self):
-        fdm = CreateFDM(self.sandbox)
-        fdm.load_script(self.sandbox.path_to_jsbsim_file('scripts',
-                                                            'c1722.xml'))
+        fdm = self.create_fdm()
+        self.load_script('c1722.xml')
         fdm['ic/gamma-deg'] = 4
         self.assertAlmostEqual(fdm['ic/gamma-deg'], 4)
 
+    # Regression test for issue #553
+    #
+    # This tests verifies that the initial value for the vehicle rotational rates
+    # P, Q, R do not depend on the position at which the vehicle is initialized.
+    # First reference : close to the North Pole heading East
+    # Second reference : above the Equator heading North
+    def testIndependenceOfInitialLocation(self):
+        script_path = self.sandbox.path_to_jsbsim_file('scripts/ball.xml')
+        tree, aircraft_name, _ = CopyAircraftDef(script_path, self.sandbox)
+        tree.write(self.sandbox('aircraft', aircraft_name, aircraft_name+'.xml'))
+        # Alter the initial conditions XML file to force the initial latitude
+        # to 90 degrees.
+        _, IC_tree, IC_file = self.getElementTrees(script_path)
+        IC_root = IC_tree.getroot()
+        lat_tag = IC_root.find('latitude')
+        psi_tag = IC_root.find('psi')
+        alt_tag = IC_root.find('altitude')
+        psi_tag.text = '90.0'  # Heading East
+        lat_tag.text = '89.9'  # Above the North Pole
+        h0 = float(alt_tag.text)
+        IC_tree.write(os.path.join('aircraft', aircraft_name, IC_file))
+
+        fdm = self.create_fdm()
+        fdm.set_aircraft_path('aircraft')
+        self.load_script('ball.xml')
+        fdm.run_ic()
+
+        p = fdm['ic/p-rad_sec']
+        q = fdm['ic/q-rad_sec']
+        r = fdm['ic/r-rad_sec']
+
+        self.delete_fdm()
+
+        # Since the equatorial radius is 70159 ft larger than the polar radius
+        # we need to decrease the altitude by the same amount in order to
+        # initialize the vehicle at the same radius.
+        alt_tag.text = str(h0-70159)
+        psi_tag.text = '0.0'  # Heading North
+        lat_tag.text = '0.0'  # Above equator
+        IC_tree.write(os.path.join('aircraft', aircraft_name, IC_file))
+
+        fdm = self.create_fdm()
+        fdm.set_aircraft_path('aircraft')
+        self.load_script('ball.xml')
+        fdm.run_ic()
+
+        self.assertAlmostEqual(fdm['ic/p-rad_sec'], p)
+        self.assertAlmostEqual(fdm['ic/q-rad_sec'], q)
+        self.assertAlmostEqual(fdm['ic/r-rad_sec'], r)
+

@jonsberndt
Copy link
Contributor

So, This is an interesting discussion. I haven't jumped into the code to look closely at this - I have some obligations today, but here are my immediate thoughts:

  1. I think we all agree that JSBSim ought to handle flying over the poles (for instance) and we need to address any NaN's (etc.) that come about.
  1. At one point I had implemented the NESC 6-DoF Simulation Check cases in JSBSim, but not sure if those made it into the codebase and regression test suite. If they did not, I want to try and get them back in there. And perhaps come up with some more simulation check cases in cooperation with or without the NESC. In any case, the check cases should also never fail. (I'm referring to the first ten atmospheric check cases described in this technical report: https://nescacademy.nasa.gov/src/flightsim/Reports/NASA-TM-2015-218675-EOM_checkcase_appendices.pdf)
  1. You guys have done an outstanding job with JSBSim for years, and I think that your thoughtfulness and attention to detail in addressing this issue gives you the best view for how to proceed.
  2. Two questions:
    a) Will making the recommended change break any existing models/initializations?
    b) Will the resulting EOM follow along with the 3rd edition of Stevens and Lewis (or some other similar treatment)?

It sounds like you are recommending to remove vOmegaLocal. It also looks like you've done a lot of work to verify why it should be removed and that the proposed fix works. I don't remember exactly why things were done the way they were back when that code was initially written. So, I'd say, go ahead with your recommended fix.

If anything in the documentation needs to be clarified or improved (or something placed on the wiki at least) that would be good to do - maybe capture some of what you wrote above and place it in the wiki. A decent portion of the JSBSim user base are engineering students, and it's good to have some kind of traceability to textbooks in common use.

That's my two cents. :-)

@bcoconni
Copy link
Member Author

  1. I think we all agree that JSBSim ought to handle flying over the poles (for instance) and we need to address any NaN's (etc.) that come about.

Sorry there again for having overreacted to your statement about the boundary case. I was afraid that this argument was used to dismiss the problem altogether. That was certainly not the case, sorry for having misinterpreted your meaning.

  1. At one point I had implemented the NESC 6-DoF Simulation Check cases in JSBSim, but not sure if those made it into the codebase and regression test suite. If they did not, I want to try and get them back in there.

That would be great ! As you suggested the NESC test cases have not yet made their way to the JSBSim regression test suite. Even if your files are not perfectly tidied up that'd be a great addition to JSBSim.

And perhaps come up with some more simulation check cases in cooperation with or without the NESC. In any case, the check cases should also never fail. (I'm referring to the first ten atmospheric check cases described in this technical report: https://nescacademy.nasa.gov/src/flightsim/Reports/NASA-TM-2015-218675-EOM_checkcase_appendices.pdf)

That's indeed what we are trying to do with the test cases other than NESC's. The more tests, the better.

  1. Two questions:
    a) Will making the recommended change break any existing models/initializations?

According to my investigations, all the tests succeed once vOmegaLocal has been removed and neither the models nor the initializations had to be modified to get that result. Actually, I had to write special test cases to trigger the bug.

b) Will the resulting EOM follow along with the 3rd edition of Stevens and Lewis (or some other similar treatment)?

As @seanmcleod pointed out in one of his comment above, Stevens & Lewis 3rd edition does not even mention w_v/e aka vOmegaLocal so I'd say we're clear about that.

It sounds like you are recommending to remove vOmegaLocal. It also looks like you've done a lot of work to verify why it should be removed and that the proposed fix works. I don't remember exactly why things were done the way they were back when that code was initially written. So, I'd say, go ahead with your recommended fix.

If anything in the documentation needs to be clarified or improved (or something placed on the wiki at least) that would be good to do - maybe capture some of what you wrote above and place it in the wiki. A decent portion of the JSBSim user base are engineering students, and it's good to have some kind of traceability to textbooks in common use.

In the short term the commits will reference this issue so that the link is maintained (at least as long as we are using GitHub). But indeed the discussion should be compiled in the wiki or in JSBSim docs.

That's my two cents. :-)

bcoconni added a commit to bcoconni/jsbsim that referenced this issue Jan 16, 2022
Improper use of the local frame rotation rate leads to the divergence vehicle rotation rates.
bcoconni added a commit to bcoconni/jsbsim that referenced this issue Jan 16, 2022
Improper use of the local frame rotation rate leads to the divergence vehicle rotation rates.
bcoconni added a commit to bcoconni/jsbsim that referenced this issue Jan 16, 2022
Improper use of the local frame rotation rate leads to the divergence vehicle rotation rates.
bcoconni added a commit to bcoconni/jsbsim that referenced this issue Jan 16, 2022
Improper use of the local frame rotation rate leads to the divergence vehicle rotation rates.
@bcoconni
Copy link
Member Author

bcoconni commented Jan 23, 2022

As @seanmcleod rightly pointed out, we still have the issue of tan(90) blowing up when the frame local is selected for initial conditions:

} else if (frame == "local") {
// Refer to Stevens and Lewis, 1.5-14a, pg. 49.
// This is the rotation rate of the "Local" frame, expressed in the local frame.
double radInv = 1.0 / position.GetRadius();
FGColumnVector3 vOmegaLocal = {radInv*vUVW_NED(eEast),
-radInv*vUVW_NED(eNorth),
-radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};
vPQR_body = Tl2b * (vAttRate + vOmegaLocal);
} else if (frame == "body") {

Given that in that particular case (frame == "local"), the rotational rate AttRate is intentionally expressed with respect to the local frame, we have no choice but to use the Stevens & Lewis' formula. AFAICS, the remaining options are:

  1. Capping the components value of vOmegaLocal to a maximum value.
  2. Issuing a warning message.
  3. Throwing an exception.
  4. Ignoring the problem: user know what they're doing, aren't ?

Any suggestion ?

@jonsberndt
Copy link
Contributor

Given that in that particular case (frame == "local"), the rotational rate AttRate is intentionally expressed with respect to the local frame, we have no choice but to use the Stevens & Lewis' formula. AFAICS, the remaining options are:

  1. Capping the components value of vOmegaLocal to a maximum value.
  2. Issuing a warning message.
  3. Throwing an exception.
  4. Ignoring the problem: user know what they're doing, aren't ?

Any suggestion ?

It seems to me that 3 & 4 are out.

Maybe I missed this from above, but can the aircraft be initialized to some rates at the poles where the frame is not the local frame? Maybe the option is to issue a warning and gracefully exit when the (hopefully rare) condition of trying to do this occurs? Or, is there special handling that can be done just at the poles?

It's early for me and I have not had my coffee yet ...

@seanmcleod
Copy link
Member

but can the aircraft be initialized to some rates at the poles where the frame is not the local frame?

Current frame options for the setting of initial rates.

bool FGInitialCondition::Load_v2(Element* document)
{
...
...
  // Initialize vehicle body rates
  // Allowable frames
  // - ECI (Earth Centered Inertial)
  // - ECEF (Earth Centered, Earth Fixed)
  // - Body

  Element* attrate_el = document->FindElement("attitude_rate");

  if (attrate_el) {

    string frame = attrate_el->GetAttributeValue("frame");
    frame = to_lower(frame);
    const FGMatrix33& Tl2b = orientation.GetT();
    FGColumnVector3 vAttRate = attrate_el->FindElementTripletConvertTo("RAD/SEC");

    if (frame == "eci") {
      FGMatrix33 Ti2l = position.GetTec2l() * Ti2ec;
      vPQR_body = Tl2b * Ti2l * (vAttRate - vOmegaEarth);
    } else if (frame == "ecef") {
      vPQR_body = Tl2b * position.GetTec2l() * vAttRate;
    } else if (frame == "local") {
      // Refer to Stevens and Lewis, 1.5-14a, pg. 49.
      // This is the rotation rate of the "Local" frame, expressed in the local frame.
      double radInv = 1.0 / position.GetRadius();
      FGColumnVector3 vOmegaLocal = {radInv*vUVW_NED(eEast),
                                    -radInv*vUVW_NED(eNorth),
                                    -radInv*vUVW_NED(eEast)*tan(position.GetLatitude())};
      vPQR_body = Tl2b * (vAttRate + vOmegaLocal);
    } else if (frame == "body") {
      vPQR_body = vAttRate;
    } else if (!frame.empty()) { // misspelling of frame

      cerr << endl << fgred << "  Attitude rate frame type: \"" << frame
           << "\" is not supported!" << reset << endl << endl;
      result = false;

    } else if (frame.empty()) {
      vPQR_body.InitMatrix();
    }

  } else { // Body frame attitude rate assumed 0 relative to local.
      vPQR_body.InitMatrix();
  }

@bcoconni
Copy link
Member Author

It seems to me that 3 & 4 are out.

Agreed.

I had some further thoughts about option 1. We should not focus on the component values of vOmegaLocal but rather on the component values of vPQR_body: users may anticipate huge values at the Poles and choose component values of AttRate that cancel out the huge values of vOmegaLocal components. Indeed what we should try to prevent is the vehicle from starting with an unanticipated huge spin about the vertical axis. In that perspective the test for option 1 should be conducted on vPQR_body.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants