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