1   /* Copyright 2002-2021 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.gnss.attitude;
18  
19  import org.hipparchus.analysis.UnivariateFunction;
20  import org.hipparchus.analysis.differentiation.DSFactory;
21  import org.hipparchus.analysis.differentiation.DerivativeStructure;
22  import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
23  import org.hipparchus.analysis.solvers.UnivariateSolverUtils;
24  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
25  import org.hipparchus.geometry.euclidean.threed.Vector3D;
26  import org.hipparchus.util.FastMath;
27  import org.hipparchus.util.FieldSinCos;
28  import org.hipparchus.util.SinCos;
29  import org.orekit.frames.Frame;
30  import org.orekit.frames.LOFType;
31  import org.orekit.frames.Transform;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.FieldAbsoluteDate;
34  import org.orekit.time.TimeStamped;
35  import org.orekit.utils.ExtendedPVCoordinatesProvider;
36  import org.orekit.utils.FieldPVCoordinates;
37  import org.orekit.utils.PVCoordinates;
38  import org.orekit.utils.PVCoordinatesProvider;
39  import org.orekit.utils.TimeStampedAngularCoordinates;
40  import org.orekit.utils.TimeStampedPVCoordinates;
41  
42  /**
43   * Boilerplate computations for GNSS attitude.
44   *
45   * <p>
46   * This class is intended to hold throw-away data pertaining to <em>one</em> call
47   * to {@link GNSSAttitudeProvider#getAttitude(org.orekit.utils.PVCoordinatesProvider,
48   * org.orekit.time.AbsoluteDate, org.orekit.frames.Frame) getAttitude}.
49   * </p>
50   *
51   * @author Luc Maisonobe
52   * @since 9.2
53   */
54  class GNSSAttitudeContext implements TimeStamped {
55  
56      /** Derivation order. */
57      private static final int ORDER = 2;
58  
59      /** Time derivation factory. */
60      private static final DSFactory FACTORY = new DSFactory(1, ORDER);
61  
62      /** Constant Y axis. */
63      private static final PVCoordinates PLUS_Y_PV =
64              new PVCoordinates(Vector3D.PLUS_J, Vector3D.ZERO, Vector3D.ZERO);
65  
66      /** Constant Z axis. */
67      private static final PVCoordinates MINUS_Z_PV =
68              new PVCoordinates(Vector3D.MINUS_K, Vector3D.ZERO, Vector3D.ZERO);
69  
70      /** Limit value below which we shoud use replace beta by betaIni. */
71      private static final double BETA_SIGN_CHANGE_PROTECTION = FastMath.toRadians(0.07);
72  
73      /** Current date. */
74      private final AbsoluteDate date;
75  
76      /** Current date. */
77      private final FieldAbsoluteDate<DerivativeStructure> dateDS;
78  
79      /** Provider for Sun position. */
80      private final ExtendedPVCoordinatesProvider sun;
81  
82      /** Provider for spacecraft position. */
83      private final PVCoordinatesProvider pvProv;
84  
85      /** Inertial frame where velocity are computed. */
86      private final Frame inertialFrame;
87  
88      /** Cosine of the angle between spacecraft and Sun direction. */
89      private final DerivativeStructure svbCos;
90  
91      /** Morning/Evening half orbit indicator. */
92      private final boolean morning;
93  
94      /** Relative orbit angle to turn center. */
95      private final DerivativeStructure delta;
96  
97      /** Spacecraft angular velocity. */
98      private double muRate;
99  
100     /** Limit cosine for the midnight turn. */
101     private double cNight;
102 
103     /** Limit cosine for the noon turn. */
104     private double cNoon;
105 
106     /** Turn time data. */
107     private TurnSpan turnSpan;
108 
109     /** Simple constructor.
110      * @param date current date
111      * @param sun provider for Sun position
112      * @param pvProv provider for spacecraft position
113      * @param inertialFrame inertial frame where velocity are computed
114      * @param turnSpan turn time data, if a turn has already been identified in the date neighborhood,
115      * null otherwise
116      */
117     GNSSAttitudeContext(final AbsoluteDate date,
118                         final ExtendedPVCoordinatesProvider sun, final PVCoordinatesProvider pvProv,
119                         final Frame inertialFrame, final TurnSpan turnSpan) {
120 
121         this.date          = date;
122         this.dateDS        = addDerivatives(date);
123         this.sun           = sun;
124         this.pvProv        = pvProv;
125         this.inertialFrame = inertialFrame;
126         final FieldPVCoordinates<DerivativeStructure> sunPVDS = sun.getPVCoordinates(dateDS, inertialFrame);
127 
128         final TimeStampedPVCoordinates svPV = pvProv.getPVCoordinates(date, inertialFrame);
129         final FieldPVCoordinates<DerivativeStructure> svPVDS  = svPV.toDerivativeStructurePV(FACTORY.getCompiler().getOrder());
130         this.svbCos  = FieldVector3D.dotProduct(sunPVDS.getPosition(), svPVDS.getPosition()).
131                        divide(sunPVDS.getPosition().getNorm().multiply(svPVDS.getPosition().getNorm()));
132         this.morning = Vector3D.dotProduct(svPV.getVelocity(), sunPVDS.getPosition().toVector3D()) >= 0.0;
133 
134         this.muRate = svPV.getAngularVelocity().getNorm();
135 
136         this.turnSpan = turnSpan;
137 
138         final DerivativeStructure absDelta;
139         if (svbCos.getValue() <= 0) {
140             // night side
141             absDelta = inOrbitPlaneAbsoluteAngle(svbCos.acos().negate().add(FastMath.PI));
142         } else {
143             // Sun side
144             absDelta = inOrbitPlaneAbsoluteAngle(svbCos.acos());
145         }
146         delta = FastMath.copySign(absDelta, -absDelta.getPartialDerivative(1));
147 
148     }
149 
150     /** Convert a date, removing derivatives.
151      * @param d date to convert
152      * @return date without derivatives
153      */
154     private AbsoluteDate removeDerivatives(final FieldAbsoluteDate<DerivativeStructure> d) {
155         return d.toAbsoluteDate();
156     }
157 
158     /** Convert a date, adding derivatives.
159      * @param d date to convert
160      * @return date without derivatives
161      */
162     private FieldAbsoluteDate<DerivativeStructure> addDerivatives(final AbsoluteDate d) {
163         return new FieldAbsoluteDate<>(FACTORY.getDerivativeField(), d).
164                shiftedBy(FACTORY.variable(0, 0.0));
165     }
166 
167     /** Compute nominal yaw steering.
168      * @param d computation date
169      * @return nominal yaw steering
170      */
171     public TimeStampedAngularCoordinates nominalYaw(final AbsoluteDate d) {
172         final PVCoordinates svPV = pvProv.getPVCoordinates(d, inertialFrame);
173         return new TimeStampedAngularCoordinates(d,
174                                                  svPV.normalize(),
175                                                  PVCoordinates.crossProduct(sun.getPVCoordinates(d, inertialFrame), svPV).normalize(),
176                                                  MINUS_Z_PV,
177                                                  PLUS_Y_PV,
178                                                  1.0e-9);
179     }
180 
181     /** Compute Sun elevation.
182      * @param d computation date
183      * @return Sun elevation
184      */
185     public double beta(final AbsoluteDate d) {
186         final TimeStampedPVCoordinates svPV = pvProv.getPVCoordinates(d, inertialFrame);
187         return 0.5 * FastMath.PI - Vector3D.angle(sun.getPVCoordinates(d, inertialFrame).getPosition(), svPV.getMomentum());
188     }
189 
190     /** Compute Sun elevation.
191      * @param d computation date
192      * @return Sun elevation
193      */
194     private DerivativeStructure betaDS(final FieldAbsoluteDate<DerivativeStructure> d) {
195         final TimeStampedPVCoordinates svPV = pvProv.getPVCoordinates(removeDerivatives(d), inertialFrame);
196         final FieldPVCoordinates<DerivativeStructure> svPVDS  = svPV.toDerivativeStructurePV(FACTORY.getCompiler().getOrder());
197         return FieldVector3D.angle(sun.getPVCoordinates(d, inertialFrame).getPosition(), svPVDS.getMomentum()).
198                negate().
199                add(0.5 * FastMath.PI);
200     }
201 
202     /** Compute Sun elevation.
203      * @return Sun elevation
204      */
205     public DerivativeStructure betaDS() {
206         return betaDS(dateDS);
207     }
208 
209     /** {@inheritDoc} */
210     @Override
211     public AbsoluteDate getDate() {
212         return date;
213     }
214 
215     /** Get the turn span.
216      * @return turn span, may be null if context is outside of turn
217      */
218     public TurnSpan getTurnSpan() {
219         return turnSpan;
220     }
221 
222     /** Get the cosine of the angle between spacecraft and Sun direction.
223      * @return cosine of the angle between spacecraft and Sun direction.
224      */
225     public double getSVBcos() {
226         return svbCos.getValue();
227     }
228 
229     /** Get a Sun elevation angle that does not change sign within the turn.
230      * <p>
231      * This method either returns the current beta or replaces it with the
232      * value at turn start, so the sign remains constant throughout the
233      * turn. As of 9.2, it is used for GPS, Glonass and Galileo.
234      * </p>
235      * @return secured Sun elevation angle
236      * @see #beta(AbsoluteDate)
237      * @see #betaDS(FieldAbsoluteDate)
238      */
239     public double getSecuredBeta() {
240         final double beta = beta(getDate());
241         return FastMath.abs(beta) < BETA_SIGN_CHANGE_PROTECTION ?
242                beta(turnSpan.getTurnStartDate()) :
243                beta;
244     }
245 
246     /** Check if a linear yaw model is still active or if we already reached target yaw.
247      * @param linearPhi value of the linear yaw model
248      * @param phiDot slope of the linear yaw model
249      * @return true if linear model is still active
250      */
251     public boolean linearModelStillActive(final double linearPhi, final double phiDot) {
252         final double dt0 = turnSpan.getTurnEndDate().durationFrom(date);
253         final UnivariateFunction yawReached = dt -> {
254             final AbsoluteDate  t       = date.shiftedBy(dt);
255             final Vector3D      pSun    = sun.getPVCoordinates(t, inertialFrame).getPosition();
256             final PVCoordinates pv      = pvProv.getPVCoordinates(t, inertialFrame);
257             final Vector3D      pSat    = pv.getPosition();
258             final Vector3D      targetX = Vector3D.crossProduct(pSat, Vector3D.crossProduct(pSun, pSat)).normalize();
259 
260             final double        phi         = linearPhi + dt * phiDot;
261             final SinCos        sc          = FastMath.sinCos(phi);
262             final Vector3D      pU          = pv.getPosition().normalize();
263             final Vector3D      mU          = pv.getMomentum().normalize();
264             final Vector3D      omega       = new Vector3D(-phiDot, pU);
265             final Vector3D      currentX    = new Vector3D(-sc.sin(), mU, -sc.cos(), Vector3D.crossProduct(pU, mU));
266             final Vector3D      currentXDot = Vector3D.crossProduct(omega, currentX);
267 
268             return Vector3D.dotProduct(targetX, currentXDot);
269         };
270         final double fullTurn = 2 * FastMath.PI / FastMath.abs(phiDot);
271         final double dtMin    = FastMath.min(turnSpan.getTurnStartDate().durationFrom(date), dt0 - 60.0);
272         final double dtMax    = FastMath.max(dtMin + fullTurn, dt0 + 60.0);
273         double[] bracket = UnivariateSolverUtils.bracket(yawReached, dt0,
274                                                          dtMin, dtMax, fullTurn / 100, 1.0, 100);
275         if (yawReached.value(bracket[0]) <= 0.0) {
276             // we have bracketed the wrong crossing
277             bracket = UnivariateSolverUtils.bracket(yawReached, 0.5 * (bracket[0] + bracket[1] + fullTurn),
278                                                     bracket[1], bracket[1] + fullTurn, fullTurn / 100, 1.0, 100);
279         }
280         final double dt = new BracketingNthOrderBrentSolver(1.0e-3, 5).
281                           solve(100, yawReached, bracket[0], bracket[1]);
282         turnSpan.updateEnd(date.shiftedBy(dt), date);
283 
284         return dt > 0.0;
285 
286     }
287 
288     /** Set up the midnight/noon turn region.
289      * @param cosNight limit cosine for the midnight turn
290      * @param cosNoon limit cosine for the noon turn
291      * @return true if spacecraft is in the midnight/noon turn region
292      */
293     public boolean setUpTurnRegion(final double cosNight, final double cosNoon) {
294         this.cNight = cosNight;
295         this.cNoon  = cosNoon;
296 
297         if (svbCos.getValue() < cNight || svbCos.getValue() > cNoon) {
298             // we are within turn triggering zone
299             return true;
300         } else {
301             // we are outside of turn triggering zone,
302             // but we may still be trying to recover nominal attitude at the end of a turn
303             return inTurnTimeRange();
304         }
305 
306     }
307 
308     /** Get the relative orbit angle to turn center.
309      * @return relative orbit angle to turn center
310      */
311     public DerivativeStructure getDeltaDS() {
312         return delta;
313     }
314 
315     /** Get the orbit angle since solar midnight.
316      * @return orbit angle since solar midnight
317      */
318     public double getOrbitAngleSinceMidnight() {
319         final double absAngle = inOrbitPlaneAbsoluteAngle(FastMath.PI - FastMath.acos(svbCos.getValue()));
320         return morning ? absAngle : -absAngle;
321     }
322 
323     /** Check if spacecraft is in the half orbit closest to Sun.
324      * @return true if spacecraft is in the half orbit closest to Sun
325      */
326     public boolean inSunSide() {
327         return svbCos.getValue() > 0;
328     }
329 
330     /** Get yaw at turn start.
331      * @param sunBeta Sun elevation above orbital plane
332      * (it <em>may</em> be different from {@link #getBeta()} in
333      * some special cases)
334      * @return yaw at turn start
335      */
336     public double getYawStart(final double sunBeta) {
337         final double halfSpan = 0.5 * turnSpan.getTurnDuration() * muRate;
338         return computePhi(sunBeta, FastMath.copySign(halfSpan, svbCos.getValue()));
339     }
340 
341     /** Get yaw at turn end.
342      * @param sunBeta Sun elevation above orbital plane
343      * (it <em>may</em> be different from {@link #getBeta()} in
344      * some special cases)
345      * @return yaw at turn end
346      */
347     public double getYawEnd(final double sunBeta) {
348         final double halfSpan = 0.5 * turnSpan.getTurnDuration() * muRate;
349         return computePhi(sunBeta, FastMath.copySign(halfSpan, -svbCos.getValue()));
350     }
351 
352     /** Compute yaw rate.
353      * @param sunBeta Sun elevation above orbital plane
354      * (it <em>may</em> be different from {@link #getBeta()} in
355      * some special cases)
356      * @return yaw rate
357      */
358     public double yawRate(final double sunBeta) {
359         return (getYawEnd(sunBeta) - getYawStart(sunBeta)) / turnSpan.getTurnDuration();
360     }
361 
362     /** Get the spacecraft angular velocity.
363      * @return spacecraft angular velocity
364      */
365     public double getMuRate() {
366         return muRate;
367     }
368 
369     /** Project a spacecraft/Sun angle into orbital plane.
370      * <p>
371      * This method is intended to find the limits of the noon and midnight
372      * turns in orbital plane. The return angle is signed, depending on the
373      * spacecraft being before or after turn middle point.
374      * </p>
375      * @param angle spacecraft/Sun angle (or spacecraft/opposite-of-Sun)
376      * @return angle projected into orbital plane, always positive
377      */
378     private DerivativeStructure inOrbitPlaneAbsoluteAngle(final DerivativeStructure angle) {
379         return FastMath.acos(FastMath.cos(angle).divide(FastMath.cos(beta(getDate()))));
380     }
381 
382     /** Project a spacecraft/Sun angle into orbital plane.
383      * <p>
384      * This method is intended to find the limits of the noon and midnight
385      * turns in orbital plane. The return angle is always positive. The
386      * correct sign to apply depends on the spacecraft being before or
387      * after turn middle point.
388      * </p>
389      * @param angle spacecraft/Sun angle (or spacecraft/opposite-of-Sun)
390      * @return angle projected into orbital plane, always positive
391      */
392     public double inOrbitPlaneAbsoluteAngle(final double angle) {
393         return FastMath.acos(FastMath.cos(angle) / FastMath.cos(beta(getDate())));
394     }
395 
396     /** Compute yaw.
397      * @param sunBeta Sun elevation above orbital plane
398      * (it <em>may</em> be different from {@link #getBeta()} in
399      * some special cases)
400      * @param inOrbitPlaneAngle in orbit angle between spacecraft
401      * and Sun (or opposite of Sun) projection
402      * @return yaw angle
403      */
404     public double computePhi(final double sunBeta, final double inOrbitPlaneAngle) {
405         return FastMath.atan2(-FastMath.tan(sunBeta), FastMath.sin(inOrbitPlaneAngle));
406     }
407 
408     /** Set turn half span and compute corresponding turn time range.
409      * @param halfSpan half span of the turn region, as an angle in orbit plane
410      * @param endMargin margin in seconds after turn end
411      */
412     public void setHalfSpan(final double halfSpan, final double endMargin) {
413 
414         final AbsoluteDate start = date.shiftedBy((delta.getValue() - halfSpan) / muRate);
415         final AbsoluteDate end   = date.shiftedBy((delta.getValue() + halfSpan) / muRate);
416         final AbsoluteDate estimationDate = getDate();
417 
418         if (turnSpan == null) {
419             turnSpan = new TurnSpan(start, end, estimationDate, endMargin);
420         } else {
421             turnSpan.updateStart(start, estimationDate);
422             turnSpan.updateEnd(end, estimationDate);
423         }
424     }
425 
426     /** Check if context is within turn range.
427      * @return true if context is within range extended by end margin
428      */
429     public boolean inTurnTimeRange() {
430         return turnSpan != null && turnSpan.inTurnTimeRange(getDate());
431     }
432 
433     /** Get elapsed time since turn start.
434      * @return elapsed time from turn start to current date
435      */
436     public double timeSinceTurnStart() {
437         return getDate().durationFrom(turnSpan.getTurnStartDate());
438     }
439 
440     /** Generate an attitude with turn-corrected yaw.
441      * @param yaw yaw value to apply
442      * @param yawDot yaw first time derivative
443      * @return attitude with specified yaw
444      */
445     public TimeStampedAngularCoordinates turnCorrectedAttitude(final double yaw, final double yawDot) {
446         return turnCorrectedAttitude(FACTORY.build(yaw, yawDot, 0.0));
447     }
448 
449     /** Generate an attitude with turn-corrected yaw.
450      * @param yaw yaw value to apply
451      * @return attitude with specified yaw
452      */
453     public TimeStampedAngularCoordinates turnCorrectedAttitude(final DerivativeStructure yaw) {
454 
455         // Earth pointing (Z aligned with position) with linear yaw (momentum with known cos/sin in the X/Y plane)
456         final PVCoordinates svPV          = pvProv.getPVCoordinates(date, inertialFrame);
457         final Vector3D      p             = svPV.getPosition();
458         final Vector3D      v             = svPV.getVelocity();
459         final Vector3D      a             = svPV.getAcceleration();
460         final double        r2            = p.getNormSq();
461         final double        r             = FastMath.sqrt(r2);
462         final Vector3D      keplerianJerk = new Vector3D(-3 * Vector3D.dotProduct(p, v) / r2, a, -a.getNorm() / r, v);
463         final PVCoordinates velocity      = new PVCoordinates(v, a, keplerianJerk);
464         final PVCoordinates momentum      = PVCoordinates.crossProduct(svPV, velocity);
465 
466         final FieldSinCos<DerivativeStructure> sc = FastMath.sinCos(yaw);
467         final DerivativeStructure c = sc.cos().negate();
468         final DerivativeStructure s = sc.sin().negate();
469         final Vector3D m0 = new Vector3D(s.getValue(),              c.getValue(),              0.0);
470         final Vector3D m1 = new Vector3D(s.getPartialDerivative(1), c.getPartialDerivative(1), 0.0);
471         final Vector3D m2 = new Vector3D(s.getPartialDerivative(2), c.getPartialDerivative(2), 0.0);
472         return new TimeStampedAngularCoordinates(date,
473                                                  svPV.normalize(), momentum.normalize(),
474                                                  MINUS_Z_PV, new PVCoordinates(m0, m1, m2),
475                                                  1.0e-9);
476 
477     }
478 
479     /** Compute Orbit Normal (ON) yaw.
480      * @return Orbit Normal yaw, using inertial frame as reference
481      */
482     public TimeStampedAngularCoordinates orbitNormalYaw() {
483         final Transform t = LOFType.LVLH_CCSDS.transformFromInertial(date, pvProv.getPVCoordinates(date, inertialFrame));
484         return new TimeStampedAngularCoordinates(date,
485                                                  t.getRotation(),
486                                                  t.getRotationRate(),
487                                                  t.getRotationAcceleration());
488     }
489 
490 }