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