1   /* Copyright 2002-2024 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.forces.radiation;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
21  import org.hipparchus.geometry.euclidean.threed.Vector3D;
22  import org.hipparchus.util.FastMath;
23  import org.hipparchus.util.Precision;
24  import org.orekit.bodies.OneAxisEllipsoid;
25  import org.orekit.frames.Frame;
26  import org.orekit.propagation.FieldSpacecraftState;
27  import org.orekit.propagation.SpacecraftState;
28  import org.orekit.time.AbsoluteDate;
29  import org.orekit.time.FieldAbsoluteDate;
30  import org.orekit.utils.Constants;
31  import org.orekit.utils.ExtendedPVCoordinatesProvider;
32  import org.orekit.utils.FrameAdapter;
33  import org.orekit.utils.OccultationEngine;
34  import org.orekit.utils.ParameterDriver;
35  
36  import java.lang.reflect.Array;
37  import java.util.List;
38  
39  /** Solar radiation pressure force model.
40   * <p>
41   * Since Orekit 11.0, it is possible to take into account
42   * the eclipses generated by Moon in the solar radiation
43   * pressure force model using the
44   * {@link #addOccultingBody(ExtendedPVCoordinatesProvider, double)}
45   * method.
46   * <p>
47   * Example:<br>
48   * <code> SolarRadiationPressure srp = </code>
49   * <code>                      new SolarRadiationPressure(CelestialBodyFactory.getSun(), Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,</code>
50   * <code>                                     new IsotropicRadiationClassicalConvention(50.0, 0.5, 0.5));</code><br>
51   * <code> srp.addOccultingBody(CelestialBodyFactory.getMoon(), Constants.MOON_EQUATORIAL_RADIUS);</code><br>
52   *
53   * @author Fabien Maussion
54   * @author &Eacute;douard Delente
55   * @author V&eacute;ronique Pommier-Maurussane
56   * @author Pascal Parraud
57   */
58  public class SolarRadiationPressure extends AbstractRadiationForceModel {
59  
60      /** Reference distance for the solar radiation pressure (m). */
61      private static final double D_REF = 149597870000.0;
62  
63      /** Reference solar radiation pressure at D_REF (N/m²). */
64      private static final double P_REF = 4.56e-6;
65  
66      /** Margin to force recompute lighting ratio derivatives when we are really inside penumbra. */
67      private static final double ANGULAR_MARGIN = 1.0e-10;
68  
69      /** Threshold to decide whether the S/C frame is Sun-centered. */
70      private static final double SUN_CENTERED_FRAME_THRESHOLD = 2. * Constants.SUN_RADIUS;
71  
72      /** Reference flux normalized for a 1m distance (N). */
73      private final double kRef;
74  
75      /** Sun model. */
76      private final ExtendedPVCoordinatesProvider sun;
77  
78      /** Spacecraft. */
79      private final RadiationSensitive spacecraft;
80  
81      /** Simple constructor with default reference values.
82       * <p>When this constructor is used, the reference values are:</p>
83       * <ul>
84       *   <li>d<sub>ref</sub> = 149597870000.0 m</li>
85       *   <li>p<sub>ref</sub> = 4.56 10<sup>-6</sup> N/m²</li>
86       * </ul>
87       * @param sun Sun model
88       * @param centralBody central body shape model (for umbra/penumbra computation)
89       * @param spacecraft the object physical and geometrical information
90       * @since 12.0
91       */
92      public SolarRadiationPressure(final ExtendedPVCoordinatesProvider sun,
93                                    final OneAxisEllipsoid centralBody,
94                                    final RadiationSensitive spacecraft) {
95          this(D_REF, P_REF, sun, centralBody, spacecraft);
96      }
97  
98      /** Complete constructor.
99       * <p>Note that reference solar radiation pressure <code>pRef</code> in
100      * N/m² is linked to solar flux SF in W/m² using
101      * formula pRef = SF/c where c is the speed of light (299792458 m/s). So
102      * at 1UA a 1367 W/m² solar flux is a 4.56 10<sup>-6</sup>
103      * N/m² solar radiation pressure.</p>
104      * @param dRef reference distance for the solar radiation pressure (m)
105      * @param pRef reference solar radiation pressure at dRef (N/m²)
106      * @param sun Sun model
107      * @param centralBody central body shape model (for umbra/penumbra computation)
108      * @param spacecraft the object physical and geometrical information
109      * @since 12.0
110      */
111     public SolarRadiationPressure(final double dRef, final double pRef,
112                                   final ExtendedPVCoordinatesProvider sun,
113                                   final OneAxisEllipsoid centralBody,
114                                   final RadiationSensitive spacecraft) {
115         super(sun, centralBody);
116         this.kRef = pRef * dRef * dRef;
117         this.sun  = sun;
118         this.spacecraft = spacecraft;
119     }
120 
121     /**
122      * Getter for radiation-sensitive spacecraft.
123      * @return radiation-sensitive model
124      * @since 12.1
125      */
126     public RadiationSensitive getRadiationSensitiveSpacecraft() {
127         return spacecraft;
128     }
129 
130     /** {@inheritDoc} */
131     @Override
132     public boolean dependsOnPositionOnly() {
133         return spacecraft instanceof IsotropicRadiationClassicalConvention || spacecraft instanceof IsotropicRadiationCNES95Convention || spacecraft instanceof IsotropicRadiationSingleCoefficient;
134     }
135 
136     /** {@inheritDoc} */
137     @Override
138     public Vector3D acceleration(final SpacecraftState s, final double[] parameters) {
139 
140         final AbsoluteDate date         = s.getDate();
141         final Frame        frame        = s.getFrame();
142         final Vector3D     position     = s.getPosition();
143         final Vector3D     sunPosition  = sun.getPosition(date, frame);
144         final Vector3D     sunSatVector = position.subtract(sunPosition);
145         final double       r2           = sunSatVector.getNormSq();
146 
147         // compute flux
148         final double   ratio = getLightingRatio(s, sunPosition);
149         final double   rawP  = ratio  * kRef / r2;
150         final Vector3D flux  = new Vector3D(rawP / FastMath.sqrt(r2), sunSatVector);
151 
152         return spacecraft.radiationPressureAcceleration(s, flux, parameters);
153 
154     }
155 
156     /** {@inheritDoc} */
157     @Override
158     public <T extends CalculusFieldElement<T>> FieldVector3D<T> acceleration(final FieldSpacecraftState<T> s,
159                                                                              final T[] parameters) {
160 
161         final FieldAbsoluteDate<T> date         = s.getDate();
162         final Frame                frame        = s.getFrame();
163         final FieldVector3D<T>     position     = s.getPosition();
164         final FieldVector3D<T>     sunPosition  = sun.getPosition(date, frame);
165         final FieldVector3D<T>     sunSatVector = position.subtract(sunPosition);
166         final T                    r2           = sunSatVector.getNormSq();
167 
168         // compute flux
169         final T                ratio = getLightingRatio(s, sunPosition);
170         final T                rawP  = ratio.multiply(kRef).divide(r2);
171         final FieldVector3D<T> flux  = new FieldVector3D<>(rawP.divide(r2.sqrt()), sunSatVector);
172 
173         return spacecraft.radiationPressureAcceleration(s, flux, parameters);
174 
175     }
176 
177     /** Check whether the frame is considerer Sun-centered.
178      *
179      * @param sunPositionInFrame Sun position in frame to test
180      * @return true if frame is considered Sun-centered
181      * @since 12.0
182      */
183     private boolean isSunCenteredFrame(final Vector3D sunPositionInFrame) {
184         // Frame is considered Sun-centered if Sun (or Solar System barycenter) position
185         // in that frame is smaller than SUN_CENTERED_FRAME_THRESHOLD
186         return sunPositionInFrame.getNorm() < SUN_CENTERED_FRAME_THRESHOLD;
187     }
188 
189 
190     /** Get the lighting ratio ([0-1]).
191      * @param state spacecraft state
192      * @param sunPosition Sun position in S/C frame at S/C date
193      * @return lighting ratio
194      * @since 12.0 added to avoid numerous call to sun.getPosition(...)
195      */
196     private double getLightingRatio(final SpacecraftState state, final Vector3D sunPosition) {
197 
198         // Check if S/C frame is Sun-centered
199         if (isSunCenteredFrame(sunPosition)) {
200             // We are in fact computing a trajectory around Sun (or solar system barycenter),
201             // not around a planet, we consider lighting ratio will always be 1
202             return 1.0;
203         }
204 
205         final List<OccultationEngine> occultingBodies = getOccultingBodies();
206         final int n = occultingBodies.size();
207 
208         final OccultationEngine.OccultationAngles[] angles = new OccultationEngine.OccultationAngles[n];
209         for (int i = 0; i < n; ++i) {
210             angles[i] = occultingBodies.get(i).angles(state);
211         }
212         final double alphaSunSq = angles[0].getOccultedApparentRadius() * angles[0].getOccultedApparentRadius();
213 
214         double result = 0.0;
215         for (int i = 0; i < n; ++i) {
216 
217             // compute lighting ratio considering one occulting body only
218             final OccultationEngine oi  = occultingBodies.get(i);
219             final double lightingRatioI = maskingRatio(angles[i]);
220             if (lightingRatioI == 0.0) {
221                 // body totally occults Sun, total eclipse is occurring.
222                 return 0.0;
223             }
224             result += lightingRatioI;
225 
226             // Mutual occulting body eclipse ratio computations between first and secondary bodies
227             for (int j = i + 1; j < n; ++j) {
228 
229                 final OccultationEngine oj = occultingBodies.get(j);
230                 final double lightingRatioJ = maskingRatio(angles[j]);
231                 if (lightingRatioJ == 0.0) {
232                     // Secondary body totally occults Sun, no more computations are required, total eclipse is occurring.
233                     return 0.0;
234                 } else if (lightingRatioJ != 1) {
235                     // Secondary body partially occults Sun
236 
237                     final OccultationEngine oij = new OccultationEngine(new FrameAdapter(oi.getOcculting().getBodyFrame()),
238                                                                         oi.getOcculting().getEquatorialRadius(),
239                                                                         oj.getOcculting());
240                     final OccultationEngine.OccultationAngles aij = oij.angles(state);
241                     final double maskingRatioIJ = maskingRatio(aij);
242                     final double alphaJSq       = aij.getOccultedApparentRadius() * aij.getOccultedApparentRadius();
243 
244                     final double mutualEclipseCorrection = (1 - maskingRatioIJ) * alphaJSq / alphaSunSq;
245                     result -= mutualEclipseCorrection;
246 
247                 }
248 
249             }
250         }
251 
252         // Final term
253         result -= n - 1;
254 
255         return result;
256     }
257 
258     /** Get the lighting ratio ([0-1]).
259      * @param state spacecraft state
260      * @return lighting ratio
261      * @since 7.1
262      */
263     public double getLightingRatio(final SpacecraftState state) {
264         return getLightingRatio(state, sun.getPosition(state.getDate(), state.getFrame()));
265     }
266 
267     /** Get the masking ratio ([0-1]) considering one pair of bodies.
268      * @param angles occultation angles
269      * @return masking ratio: 0.0 body fully masked, 1.0 body fully visible
270      * @since 12.0
271      */
272     private double maskingRatio(final OccultationEngine.OccultationAngles angles) {
273 
274         // Sat-Occulted/ Sat-Occulting angle
275         final double sunSatCentralBodyAngle = angles.getSeparation();
276 
277         // Occulting apparent radius
278         final double alphaCentral = angles.getLimbRadius();
279 
280         // Occulted apparent radius
281         final double alphaSun = angles.getOccultedApparentRadius();
282 
283         // Is the satellite in complete umbra ?
284         if (sunSatCentralBodyAngle - alphaCentral + alphaSun <= ANGULAR_MARGIN) {
285             return 0.0;
286         } else if (sunSatCentralBodyAngle - alphaCentral - alphaSun < -ANGULAR_MARGIN) {
287             // Compute a masking ratio in penumbra
288             final double sEA2    = sunSatCentralBodyAngle * sunSatCentralBodyAngle;
289             final double oo2sEA  = 1.0 / (2. * sunSatCentralBodyAngle);
290             final double aS2     = alphaSun * alphaSun;
291             final double aE2     = alphaCentral * alphaCentral;
292             final double aE2maS2 = aE2 - aS2;
293 
294             final double alpha1  = (sEA2 - aE2maS2) * oo2sEA;
295             final double alpha2  = (sEA2 + aE2maS2) * oo2sEA;
296 
297             // Protection against numerical inaccuracy at boundaries
298             final double almost0 = Precision.SAFE_MIN;
299             final double almost1 = FastMath.nextDown(1.0);
300             final double a1oaS   = FastMath.min(almost1, FastMath.max(-almost1, alpha1 / alphaSun));
301             final double aS2ma12 = FastMath.max(almost0, aS2 - alpha1 * alpha1);
302             final double a2oaE   = FastMath.min(almost1, FastMath.max(-almost1, alpha2 / alphaCentral));
303             final double aE2ma22 = FastMath.max(almost0, aE2 - alpha2 * alpha2);
304 
305             final double P1 = aS2 * FastMath.acos(a1oaS) - alpha1 * FastMath.sqrt(aS2ma12);
306             final double P2 = aE2 * FastMath.acos(a2oaE) - alpha2 * FastMath.sqrt(aE2ma22);
307 
308             return 1. - (P1 + P2) / (FastMath.PI * aS2);
309         } else {
310             return 1.0;
311         }
312 
313     }
314 
315     /** Get the lighting ratio ([0-1]).
316      * @param state spacecraft state
317      * @param sunPosition Sun position in S/C frame at S/C date
318      * @param <T> extends CalculusFieldElement
319      * @return lighting ratio
320      * @since 12.0 added to avoid numerous call to sun.getPosition(...)
321      */
322     private <T extends CalculusFieldElement<T>> T getLightingRatio(final FieldSpacecraftState<T> state, final FieldVector3D<T> sunPosition) {
323 
324         final T one  = state.getDate().getField().getOne();
325         if (isSunCenteredFrame(sunPosition.toVector3D())) {
326             // We are in fact computing a trajectory around Sun (or solar system barycenter),
327             // not around a planet, we consider lighting ratio will always be 1
328             return one;
329         }
330         final T zero = state.getDate().getField().getZero();
331         final List<OccultationEngine> occultingBodies = getOccultingBodies();
332         final int n = occultingBodies.size();
333 
334         @SuppressWarnings("unchecked")
335         final OccultationEngine.FieldOccultationAngles<T>[] angles =
336         (OccultationEngine.FieldOccultationAngles<T>[]) Array.newInstance(OccultationEngine.FieldOccultationAngles.class, n);
337         for (int i = 0; i < n; ++i) {
338             angles[i] = occultingBodies.get(i).angles(state);
339         }
340         final T alphaSunSq = angles[0].getOccultedApparentRadius().multiply(angles[0].getOccultedApparentRadius());
341 
342         T result = state.getDate().getField().getZero();
343         for (int i = 0; i < n; ++i) {
344 
345             // compute lighting ratio considering one occulting body only
346             final OccultationEngine oi  = occultingBodies.get(i);
347             final T lightingRatioI = maskingRatio(angles[i]);
348             if (lightingRatioI.isZero()) {
349                 // body totally occults Sun, total eclipse is occurring.
350                 return zero;
351             }
352             result = result.add(lightingRatioI);
353 
354             // Mutual occulting body eclipse ratio computations between first and secondary bodies
355             for (int j = i + 1; j < n; ++j) {
356 
357                 final OccultationEngine oj = occultingBodies.get(j);
358                 final T lightingRatioJ = maskingRatio(angles[j]);
359                 if (lightingRatioJ.isZero()) {
360                     // Secondary body totally occults Sun, no more computations are required, total eclipse is occurring.
361                     return zero;
362                 } else if (lightingRatioJ.getReal() != 1) {
363                     // Secondary body partially occults Sun
364 
365                     final OccultationEngine oij = new OccultationEngine(new FrameAdapter(oi.getOcculting().getBodyFrame()),
366                                                                         oi.getOcculting().getEquatorialRadius(),
367                                                                         oj.getOcculting());
368                     final OccultationEngine.FieldOccultationAngles<T> aij = oij.angles(state);
369                     final T maskingRatioIJ = maskingRatio(aij);
370                     final T alphaJSq       = aij.getOccultedApparentRadius().multiply(aij.getOccultedApparentRadius());
371 
372                     final T mutualEclipseCorrection = one.subtract(maskingRatioIJ).multiply(alphaJSq).divide(alphaSunSq);
373                     result = result.subtract(mutualEclipseCorrection);
374 
375                 }
376 
377             }
378         }
379 
380         // Final term
381         result = result.subtract(n - 1);
382 
383         return result;
384     }
385 
386     /** Get the lighting ratio ([0-1]).
387      * @param state spacecraft state
388      * @param <T> extends CalculusFieldElement
389      * @return lighting ratio
390      * @since 7.1
391      */
392     public <T extends CalculusFieldElement<T>> T getLightingRatio(final FieldSpacecraftState<T> state) {
393         return getLightingRatio(state, sun.getPosition(state.getDate(), state.getFrame()));
394     }
395 
396     /** Get the masking ratio ([0-1]) considering one pair of bodies.
397      * @param angles occultation angles
398      * @param <T> type of the field elements
399      * @return masking ratio: 0.0 body fully masked, 1.0 body fully visible
400      * @since 12.0
401      */
402     private <T extends CalculusFieldElement<T>> T maskingRatio(final OccultationEngine.FieldOccultationAngles<T> angles) {
403 
404 
405         // Sat-Occulted/ Sat-Occulting angle
406         final T occultedSatOcculting = angles.getSeparation();
407 
408         // Occulting apparent radius
409         final T alphaOcculting = angles.getLimbRadius();
410 
411         // Occulted apparent radius
412         final T alphaOcculted = angles.getOccultedApparentRadius();
413 
414         // Is the satellite in complete umbra ?
415         if (occultedSatOcculting.getReal() - alphaOcculting.getReal() + alphaOcculted.getReal() <= ANGULAR_MARGIN) {
416             return occultedSatOcculting.getField().getZero();
417         } else if (occultedSatOcculting.getReal() - alphaOcculting.getReal() - alphaOcculted.getReal() < -ANGULAR_MARGIN) {
418             // Compute a masking ratio in penumbra
419             final T sEA2    = occultedSatOcculting.multiply(occultedSatOcculting);
420             final T oo2sEA  = occultedSatOcculting.multiply(2).reciprocal();
421             final T aS2     = alphaOcculted.multiply(alphaOcculted);
422             final T aE2     = alphaOcculting.multiply(alphaOcculting);
423             final T aE2maS2 = aE2.subtract(aS2);
424 
425             final T alpha1  = sEA2.subtract(aE2maS2).multiply(oo2sEA);
426             final T alpha2  = sEA2.add(aE2maS2).multiply(oo2sEA);
427 
428             // Protection against numerical inaccuracy at boundaries
429             final double almost0 = Precision.SAFE_MIN;
430             final double almost1 = FastMath.nextDown(1.0);
431             final T a1oaS   = min(almost1, max(-almost1, alpha1.divide(alphaOcculted)));
432             final T aS2ma12 = max(almost0, aS2.subtract(alpha1.multiply(alpha1)));
433             final T a2oaE   = min(almost1, max(-almost1, alpha2.divide(alphaOcculting)));
434             final T aE2ma22 = max(almost0, aE2.subtract(alpha2.multiply(alpha2)));
435 
436             final T P1 = aS2.multiply(a1oaS.acos()).subtract(alpha1.multiply(aS2ma12.sqrt()));
437             final T P2 = aE2.multiply(a2oaE.acos()).subtract(alpha2.multiply(aE2ma22.sqrt()));
438 
439             return occultedSatOcculting.getField().getOne().subtract(P1.add(P2).divide(aS2.multiply(occultedSatOcculting.getPi())));
440         } else {
441             return occultedSatOcculting.getField().getOne();
442         }
443 
444     }
445 
446     /** {@inheritDoc} */
447     @Override
448     public List<ParameterDriver> getParametersDrivers() {
449         return spacecraft.getRadiationParametersDrivers();
450     }
451 
452     /** Compute min of two values, one double and one field element.
453      * @param d double value
454      * @param f field element
455      * @param <T> type of the field elements
456      * @return min value
457      */
458     private <T extends CalculusFieldElement<T>> T min(final double d, final T f) {
459         return (f.getReal() > d) ? f.getField().getZero().newInstance(d) : f;
460     }
461 
462     /** Compute max of two values, one double and one field element.
463      * @param d double value
464      * @param f field element
465      * @param <T> type of the field elements
466      * @return max value
467      */
468     private <T extends CalculusFieldElement<T>> T max(final double d, final T f) {
469         return (f.getReal() <= d) ? f.getField().getZero().newInstance(d) : f;
470     }
471 
472 }