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