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