1   /* Copyright 2002-2023 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 java.lang.reflect.Array;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.stream.Stream;
24  
25  import org.hipparchus.CalculusFieldElement;
26  import org.hipparchus.Field;
27  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
28  import org.hipparchus.geometry.euclidean.threed.Vector3D;
29  import org.hipparchus.ode.events.Action;
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.MathArrays;
32  import org.orekit.bodies.OneAxisEllipsoid;
33  import org.orekit.forces.ForceModel;
34  import org.orekit.frames.Frame;
35  import org.orekit.propagation.events.EclipseDetector;
36  import org.orekit.propagation.events.EventDetector;
37  import org.orekit.propagation.events.FieldEclipseDetector;
38  import org.orekit.propagation.events.FieldEventDetector;
39  import org.orekit.utils.Constants;
40  import org.orekit.utils.ExtendedPVCoordinatesProvider;
41  import org.orekit.utils.ExtendedPVCoordinatesProviderAdapter;
42  import org.orekit.utils.OccultationEngine;
43  
44  /**
45   * Base class for radiation force models.
46   * @see SolarRadiationPressure
47   * @see ECOM2
48   * @since 10.2
49   */
50  public abstract class AbstractRadiationForceModel implements ForceModel {
51  
52      /** Margin to force recompute lighting ratio derivatives when we are really inside penumbra. */
53      private static final double ANGULAR_MARGIN = 1.0e-10;
54  
55      /** Max check interval for eclipse detectors. */
56      private static final double ECLIPSE_MAX_CHECK = 60.0;
57  
58      /** Threshold for eclipse detectors. */
59      private static final double ECLIPSE_THRESHOLD = 1.0e-7; // this is consistent with ANGULAR_MARGIN = 10⁻¹⁰ rad for LEO
60  
61      /** Flatness for spherical bodies. */
62      private static final double SPHERICAL_BODY_FLATNESS = 0.0;
63  
64      /** Prefix for occulting bodies frames names. */
65      private static final String OCCULTING_PREFIX = "occulting-";
66  
67      /** Occulting bodies (central body is always the first one).
68       * @since 12.0
69       */
70      private final List<OccultationEngine> occultingBodies;
71  
72      /**
73       * Default constructor.
74       * Only central body is considered.
75       * @param sun Sun model
76       * @param centralBody central body shape model (for umbra/penumbra computation)
77       * @since 12.0
78       */
79      protected AbstractRadiationForceModel(final ExtendedPVCoordinatesProvider sun, final OneAxisEllipsoid centralBody) {
80          // in most cases, there will be only Earth, sometimes also Moon so an initial capacity of 2 is appropriate
81          occultingBodies = new ArrayList<>(2);
82          occultingBodies.add(new OccultationEngine(sun, Constants.SUN_RADIUS, centralBody));
83      }
84  
85      /** {@inheritDoc} */
86      @Override
87      public boolean dependsOnPositionOnly() {
88          return false;
89      }
90  
91      /** {@inheritDoc} */
92      @Override
93      public Stream<EventDetector> getEventDetectors() {
94          final EventDetector[] detectors = new EventDetector[2 * occultingBodies.size()];
95          for (int i = 0; i < occultingBodies.size(); ++i) {
96              final OccultationEngine occulting = occultingBodies.get(i);
97              detectors[2 * i]     = new EclipseDetector(occulting).
98                                     withUmbra().
99                                     withMargin(-ANGULAR_MARGIN).
100                                    withMaxCheck(ECLIPSE_MAX_CHECK).
101                                    withThreshold(ECLIPSE_THRESHOLD).
102                                    withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
103             detectors[2 * i + 1] = new EclipseDetector(occulting).
104                                    withPenumbra().
105                                    withMargin(ANGULAR_MARGIN).
106                                    withMaxCheck(ECLIPSE_MAX_CHECK).
107                                    withThreshold(ECLIPSE_THRESHOLD).
108                                    withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
109         }
110         // Fusion between Date detector for parameter driver span change and
111         // Detector for umbra / penumbra events
112         return Stream.concat(Stream.of(detectors), ForceModel.super.getEventDetectors());
113     }
114 
115     /** {@inheritDoc} */
116     @Override
117     public <T extends CalculusFieldElement<T>> Stream<FieldEventDetector<T>> getFieldEventDetectors(final Field<T> field) {
118         final T zero = field.getZero();
119         @SuppressWarnings("unchecked")
120         final FieldEventDetector<T>[] detectors = (FieldEventDetector<T>[]) Array.newInstance(FieldEventDetector.class,
121                                                                                               2 * occultingBodies.size());
122         for (int i = 0; i < occultingBodies.size(); ++i) {
123             final OccultationEngine occulting = occultingBodies.get(i);
124             detectors[2 * i]     = new FieldEclipseDetector<>(field, occulting).
125                                    withUmbra().
126                                    withMargin(zero.newInstance(-ANGULAR_MARGIN)).
127                                    withMaxCheck(ECLIPSE_MAX_CHECK).
128                                    withThreshold(zero.newInstance(ECLIPSE_THRESHOLD)).
129                                    withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
130             detectors[2 * i + 1] = new FieldEclipseDetector<>(field, occulting).
131                                    withPenumbra().
132                                    withMargin(zero.newInstance(ANGULAR_MARGIN)).
133                                    withMaxCheck(ECLIPSE_MAX_CHECK).
134                                    withThreshold(zero.newInstance(ECLIPSE_THRESHOLD)).
135                                    withHandler((state, detector, increasing) -> Action.RESET_DERIVATIVES);
136         }
137         return Stream.concat(Stream.of(detectors), ForceModel.super.getFieldEventDetectors(field));
138     }
139 
140     /**
141      * Get the useful angles for eclipse computation.
142      * @param position the satellite's position in the selected frame
143      * @param occultingPosition Oculting body position in the selected frame
144      * @param occultingRadius Occulting body mean radius
145      * @param occultedPosition Occulted body position in the selected frame
146      * @param occultedRadius Occulted body mean radius
147      * @return the 3 angles {(satOcculting, satOcculted), Occulting body apparent radius, Occulted body apparent radius}
148      */
149     protected double[] getGeneralEclipseAngles(final Vector3D position, final Vector3D occultingPosition, final double occultingRadius,
150                                                final Vector3D occultedPosition, final double occultedRadius) {
151         final double[] angle = new double[3];
152 
153         final Vector3D satOccultedVector = occultedPosition.subtract(position);
154         final Vector3D satOccultingVector = occultingPosition.subtract(position);
155 
156         // Sat-Occulted / Sat-Occulting angle
157         angle[0] = Vector3D.angle(satOccultedVector, satOccultingVector);
158 
159         // Occulting body apparent radius
160         angle[1] = FastMath.asin(occultingRadius / satOccultingVector.getNorm());
161 
162         // Occulted body apparent radius
163         angle[2] = FastMath.asin(occultedRadius / satOccultedVector.getNorm());
164 
165         return angle;
166     }
167 
168     /**
169      * Get the useful angles for eclipse computation.
170      * @param occultingPosition Oculting body position in the selected frame
171      * @param occultingRadius Occulting body mean radius
172      * @param occultedPosition Occulted body position in the selected frame
173      * @param occultedRadius Occulted body mean radius
174      * @param position the satellite's position in the selected frame
175      * @param <T> extends RealFieldElement
176      * @return the 3 angles {(satOcculting, satOcculted), Occulting body apparent radius, Occulted body apparent radius}
177      */
178     protected <T extends CalculusFieldElement<T>> T[] getGeneralEclipseAngles(final FieldVector3D<T> position,
179                                                                               final FieldVector3D<T> occultingPosition, final T occultingRadius,
180                                                                               final FieldVector3D<T> occultedPosition, final T occultedRadius) {
181         final T[] angle = MathArrays.buildArray(position.getX().getField(), 3);
182 
183         final FieldVector3D<T> satOccultedVector = occultedPosition.subtract(position);
184         final FieldVector3D<T> satOccultingVector = occultingPosition.subtract(position);
185 
186         // Sat-Occulted / Sat-Occulting angle
187         angle[0] = FieldVector3D.angle(satOccultedVector, satOccultingVector);
188 
189         // Occulting body apparent radius
190         angle[1] = occultingRadius.divide(satOccultingVector.getNorm()).asin();
191 
192         // Occulted body apparent radius
193         angle[2] = occultedRadius.divide(satOccultedVector.getNorm()).asin();
194 
195         return angle;
196     }
197 
198     /**
199      * Add a new occulting body.
200      * <p>
201      * Central body is already considered, it shall not be added this way.
202      * </p>
203      * @param provider body PV provider
204      * @param radius body mean radius
205      * @see #addOccultingBody(OneAxisEllipsoid)
206      */
207     public void addOccultingBody(final ExtendedPVCoordinatesProvider provider, final double radius) {
208 
209         // as parent frame for occulting body frame,
210         // we select the first inertial frame in central body hierarchy
211         Frame parent = occultingBodies.get(0).getOcculting().getBodyFrame();
212         while (!parent.isPseudoInertial()) {
213             parent = parent.getParent();
214         }
215 
216         // as the occulting body will be spherical, we can use an inertially oriented body frame
217         final Frame inertiallyOrientedBodyFrame =
218                         new ExtendedPVCoordinatesProviderAdapter(parent,
219                                                                  provider,
220                                                                  OCCULTING_PREFIX + occultingBodies.size());
221 
222         // create the spherical occulting body
223         final OneAxisEllipsoid sphericalOccultingBody =
224                         new OneAxisEllipsoid(radius, SPHERICAL_BODY_FLATNESS, inertiallyOrientedBodyFrame);
225 
226         addOccultingBody(sphericalOccultingBody);
227 
228     }
229 
230     /**
231      * Add a new occulting body.
232      * <p>
233      * Central body is already considered, it shall not be added this way.
234      * </p>
235      * @param occulting occulting body to add
236      * @see #addOccultingBody(ExtendedPVCoordinatesProvider, double)
237      * @since 12.0
238      */
239     public void addOccultingBody(final OneAxisEllipsoid occulting) {
240 
241         // retrieve Sun from the central occulting body engine
242         final OccultationEngine central = occultingBodies.get(0);
243 
244         // create a new occultation engine for the new occulting body
245         final OccultationEngine additional =
246                         new OccultationEngine(central.getOcculted(), central.getOccultedRadius(), occulting);
247 
248         // add it to the list
249         occultingBodies.add(additional);
250 
251     }
252 
253     /**
254      * Get all occulting bodies to consider.
255      * <p>
256      * The list always contains at least one element: the central body
257      * which is always the first one in the list.
258      * </p>
259      * @return immutable list of all occulting bodies to consider, including the central body
260      * @since 12.0
261      */
262     public List<OccultationEngine> getOccultingBodies() {
263         return Collections.unmodifiableList(occultingBodies);
264     }
265 
266 }