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.models.earth.ionosphere;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.nio.charset.StandardCharsets;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.regex.Pattern;
27  
28  import org.hipparchus.CalculusFieldElement;
29  import org.hipparchus.Field;
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.FieldSinCos;
32  import org.hipparchus.util.MathArrays;
33  import org.hipparchus.util.SinCos;
34  import org.orekit.annotation.DefaultDataContext;
35  import org.orekit.bodies.BodyShape;
36  import org.orekit.bodies.FieldGeodeticPoint;
37  import org.orekit.bodies.GeodeticPoint;
38  import org.orekit.data.DataContext;
39  import org.orekit.errors.OrekitException;
40  import org.orekit.errors.OrekitMessages;
41  import org.orekit.frames.TopocentricFrame;
42  import org.orekit.propagation.FieldSpacecraftState;
43  import org.orekit.propagation.SpacecraftState;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.time.DateComponents;
46  import org.orekit.time.DateTimeComponents;
47  import org.orekit.time.FieldAbsoluteDate;
48  import org.orekit.time.TimeScale;
49  import org.orekit.utils.ParameterDriver;
50  
51  /**
52   * NeQuick ionospheric delay model.
53   *
54   * @author Bryan Cazabonne
55   *
56   * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
57   *       Algorithm for Galileo Single Frequency Users. 1.2."
58   *
59   * @since 10.1
60   */
61  public class NeQuickModel implements IonosphericModel {
62  
63      /** NeQuick resources base directory. */
64      private static final String NEQUICK_BASE = "/assets/org/orekit/nequick/";
65  
66      /** Pattern for delimiting regular expressions. */
67      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
68  
69      /** Mean Earth radius in m (Ref Table 2.5.2). */
70      private static final double RE = 6371200.0;
71  
72      /** Meters to kilometers converter. */
73      private static final double M_TO_KM = 0.001;
74  
75      /** Factor for the electron density computation. */
76      private static final double DENSITY_FACTOR = 1.0e11;
77  
78      /** Factor for the path delay computation. */
79      private static final double DELAY_FACTOR = 40.3e16;
80  
81      /** The three ionospheric coefficients broadcast in the Galileo navigation message. */
82      private final double[] alpha;
83  
84      /** MODIP grid. */
85      private final double[][] stModip;
86  
87      /** Month used for loading CCIR coefficients. */
88      private int month;
89  
90      /** F2 coefficients used by the F2 layer. */
91      private double[][][] f2;
92  
93      /** Fm3 coefficients used by the F2 layer. */
94      private double[][][] fm3;
95  
96      /** UTC time scale. */
97      private final TimeScale utc;
98  
99      /**
100      * Build a new instance.
101      *
102      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
103      *
104      * @param alpha effective ionisation level coefficients
105      * @see #NeQuickModel(double[], TimeScale)
106      */
107     @DefaultDataContext
108     public NeQuickModel(final double[] alpha) {
109         this(alpha, DataContext.getDefault().getTimeScales().getUTC());
110     }
111 
112     /**
113      * Build a new instance.
114      * @param alpha effective ionisation level coefficients
115      * @param utc UTC time scale.
116      * @since 10.1
117      */
118     public NeQuickModel(final double[] alpha,
119                         final TimeScale utc) {
120         // F2 layer values
121         this.month = 0;
122         this.f2    = null;
123         this.fm3   = null;
124         // Read modip grid
125         final MODIPLoader parser = new MODIPLoader();
126         parser.loadMODIPGrid();
127         this.stModip = parser.getMODIPGrid();
128         // Ionisation level coefficients
129         this.alpha = alpha.clone();
130         this.utc = utc;
131     }
132 
133     @Override
134     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
135                             final double frequency, final double[] parameters) {
136         // Point
137         final GeodeticPoint recPoint = baseFrame.getPoint();
138         // Date
139         final AbsoluteDate date = state.getDate();
140 
141         // Reference body shape
142         final BodyShape ellipsoid = baseFrame.getParentShape();
143         // Satellite geodetic coordinates
144         final GeodeticPoint satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()), ellipsoid.getBodyFrame(), state.getDate());
145 
146         // Total Electron Content
147         final double tec = stec(date, recPoint, satPoint);
148 
149         // Ionospheric delay
150         final double factor = DELAY_FACTOR / (frequency * frequency);
151         return factor * tec;
152     }
153 
154     @Override
155     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
156                                                        final double frequency, final T[] parameters) {
157         // Date
158         final FieldAbsoluteDate<T> date = state.getDate();
159         // Point
160         final FieldGeodeticPoint<T> recPoint = baseFrame.getPoint(date.getField());
161 
162 
163         // Reference body shape
164         final BodyShape ellipsoid = baseFrame.getParentShape();
165         // Satellite geodetic coordinates
166         final FieldGeodeticPoint<T> satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()), ellipsoid.getBodyFrame(), state.getDate());
167 
168         // Total Electron Content
169         final T tec = stec(date, recPoint, satPoint);
170 
171         // Ionospheric delay
172         final double factor = DELAY_FACTOR / (frequency * frequency);
173         return tec.multiply(factor);
174     }
175 
176     @Override
177     public List<ParameterDriver> getParametersDrivers() {
178         return Collections.emptyList();
179     }
180 
181     /**
182      * This method allows the computation of the Stant Total Electron Content (STEC).
183      * <p>
184      * This method follows the Gauss algorithm exposed in section 2.5.8.2.8 of
185      * the reference document.
186      * </p>
187      * @param date current date
188      * @param recP receiver position
189      * @param satP satellite position
190      * @return the STEC in TECUnits
191      */
192     public double stec(final AbsoluteDate date, final GeodeticPoint recP, final GeodeticPoint satP) {
193 
194         // Ray-perigee parameters
195         final Ray ray = new Ray(recP, satP);
196 
197         // Load the correct CCIR file
198         final DateTimeComponents dateTime = date.getComponents(utc);
199         loadsIfNeeded(dateTime.getDate());
200 
201         // Tolerance for the integration accuracy. Defined inside the reference document, section 2.5.8.1.
202         final double h1 = recP.getAltitude();
203         final double tolerance;
204         if (h1 < 1000000.0) {
205             tolerance = 0.001;
206         } else {
207             tolerance = 0.01;
208         }
209 
210         // Integration
211         int n = 8;
212         final Segment seg1 = new Segment(n, ray);
213         double gn1 = stecIntegration(seg1, dateTime);
214         n *= 2;
215         final Segment seg2 = new Segment(n, ray);
216         double gn2 = stecIntegration(seg2, dateTime);
217 
218         int count = 1;
219         while (FastMath.abs(gn2 - gn1) > tolerance * FastMath.abs(gn1) && count < 20) {
220             gn1 = gn2;
221             n *= 2;
222             final Segment seg = new Segment(n, ray);
223             gn2 = stecIntegration(seg, dateTime);
224             count += 1;
225         }
226 
227         // If count > 20 the integration did not converge
228         if (count == 20) {
229             throw new OrekitException(OrekitMessages.STEC_INTEGRATION_DID_NOT_CONVERGE);
230         }
231 
232         // Eq. 202
233         return (gn2 + ((gn2 - gn1) / 15.0)) * 1.0e-16;
234     }
235 
236     /**
237      * This method allows the computation of the Stant Total Electron Content (STEC).
238      * <p>
239      * This method follows the Gauss algorithm exposed in section 2.5.8.2.8 of
240      * the reference document.
241      * </p>
242      * @param <T> type of the elements
243      * @param date current date
244      * @param recP receiver position
245      * @param satP satellite position
246      * @return the STEC in TECUnits
247      */
248     public <T extends CalculusFieldElement<T>> T stec(final FieldAbsoluteDate<T> date,
249                                                   final FieldGeodeticPoint<T> recP,
250                                                   final FieldGeodeticPoint<T> satP) {
251 
252         // Field
253         final Field<T> field = date.getField();
254 
255         // Ray-perigee parameters
256         final FieldRay<T> ray = new FieldRay<>(field, recP, satP);
257 
258         // Load the correct CCIR file
259         final DateTimeComponents dateTime = date.getComponents(utc);
260         loadsIfNeeded(dateTime.getDate());
261 
262         // Tolerance for the integration accuracy. Defined inside the reference document, section 2.5.8.1.
263         final T h1 = recP.getAltitude();
264         final double tolerance;
265         if (h1.getReal() < 1000000.0) {
266             tolerance = 0.001;
267         } else {
268             tolerance = 0.01;
269         }
270 
271         // Integration
272         int n = 8;
273         final FieldSegment<T> seg1 = new FieldSegment<>(field, n, ray);
274         T gn1 = stecIntegration(field, seg1, dateTime);
275         n *= 2;
276         final FieldSegment<T> seg2 = new FieldSegment<>(field, n, ray);
277         T gn2 = stecIntegration(field, seg2, dateTime);
278 
279         int count = 1;
280         while (FastMath.abs(gn2.subtract(gn1)).getReal() > FastMath.abs(gn1).multiply(tolerance).getReal() && count < 20) {
281             gn1 = gn2;
282             n *= 2;
283             final FieldSegment<T> seg = new FieldSegment<>(field, n, ray);
284             gn2 = stecIntegration(field, seg, dateTime);
285             count += 1;
286         }
287 
288         // If count > 20 the integration did not converge
289         if (count == 20) {
290             throw new OrekitException(OrekitMessages.STEC_INTEGRATION_DID_NOT_CONVERGE);
291         }
292 
293         // Eq. 202
294         return gn2.add(gn2.subtract(gn1).divide(15.0)).multiply(1.0e-16);
295     }
296 
297     /**
298      * This method perfoms the STEC integration.
299      * @param seg coordinates along the integration path
300      * @param dateTime current date and time componentns
301      * @return result of the integration
302      */
303     private double stecIntegration(final Segment seg, final DateTimeComponents dateTime) {
304         // Integration points
305         final double[] heightS    = seg.getHeights();
306         final double[] latitudeS  = seg.getLatitudes();
307         final double[] longitudeS = seg.getLongitudes();
308 
309         // Compute electron density
310         double density = 0.0;
311         for (int i = 0; i < heightS.length; i++) {
312             final NeQuickParameters parameters = new NeQuickParameters(dateTime, f2, fm3,
313                                                                        latitudeS[i], longitudeS[i],
314                                                                        alpha, stModip);
315             density += electronDensity(heightS[i], parameters);
316         }
317 
318         return 0.5 * seg.getInterval() * density;
319     }
320 
321     /**
322      * This method perfoms the STEC integration.
323      * @param <T> type of the elements
324      * @param field field of the elements
325      * @param seg coordinates along the integration path
326      * @param dateTime current date and time componentns
327      * @return result of the integration
328      */
329     private <T extends CalculusFieldElement<T>> T stecIntegration(final Field<T> field,
330                                                               final FieldSegment<T> seg,
331                                                               final DateTimeComponents dateTime) {
332         // Integration points
333         final T[] heightS    = seg.getHeights();
334         final T[] latitudeS  = seg.getLatitudes();
335         final T[] longitudeS = seg.getLongitudes();
336 
337         // Compute electron density
338         T density = field.getZero();
339         for (int i = 0; i < heightS.length; i++) {
340             final FieldNeQuickParameters<T> parameters = new FieldNeQuickParameters<>(field, dateTime, f2, fm3,
341                                                                                       latitudeS[i], longitudeS[i],
342                                                                                       alpha, stModip);
343             density = density.add(electronDensity(field, heightS[i], parameters));
344         }
345 
346         return seg.getInterval().multiply(density).multiply(0.5);
347     }
348 
349     /**
350      * Computes the electron density at a given height.
351      * @param h height in m
352      * @param parameters NeQuick model parameters
353      * @return electron density [m^-3]
354      */
355     private double electronDensity(final double h, final NeQuickParameters parameters) {
356         // Convert height in kilometers
357         final double hInKm = h * M_TO_KM;
358         // Electron density
359         final double n;
360         if (hInKm <= parameters.getHmF2()) {
361             n = bottomElectronDensity(hInKm, parameters);
362         } else {
363             n = topElectronDensity(hInKm, parameters);
364         }
365         return n;
366     }
367 
368     /**
369      * Computes the electron density at a given height.
370      * @param <T> type of the elements
371      * @param field field of the elements
372      * @param h height in m
373      * @param parameters NeQuick model parameters
374      * @return electron density [m^-3]
375      */
376     private <T extends CalculusFieldElement<T>> T electronDensity(final Field<T> field,
377                                                               final T h,
378                                                               final FieldNeQuickParameters<T> parameters) {
379         // Convert height in kilometers
380         final T hInKm = h.multiply(M_TO_KM);
381         // Electron density
382         final T n;
383         if (hInKm.getReal() <= parameters.getHmF2().getReal()) {
384             n = bottomElectronDensity(field, hInKm, parameters);
385         } else {
386             n = topElectronDensity(field, hInKm, parameters);
387         }
388         return n;
389     }
390 
391     /**
392      * Computes the electron density of the bottomside.
393      * @param h height in km
394      * @param parameters NeQuick model parameters
395      * @return the electron density N in m-3
396      */
397     private double bottomElectronDensity(final double h, final NeQuickParameters parameters) {
398 
399         // Select the relevant B parameter for the current height (Eq. 109 and 110)
400         final double be;
401         if (h > parameters.getHmE()) {
402             be = parameters.getBETop();
403         } else {
404             be = parameters.getBEBot();
405         }
406         final double bf1;
407         if (h > parameters.getHmF1()) {
408             bf1 = parameters.getB1Top();
409         } else {
410             bf1 = parameters.getB1Bot();
411         }
412         final double bf2 = parameters.getB2Bot();
413 
414         // Useful array of constants
415         final double[] ct = new double[] {
416             1.0 / bf2, 1.0 / bf1, 1.0 / be
417         };
418 
419         // Compute the exponential argument for each layer (Eq. 111 to 113)
420         // If h < 100km we use h = 100km as recommended in the reference document
421         final double   hTemp = FastMath.max(100.0, h);
422         final double   exp   = clipExp(10.0 / (1.0 + FastMath.abs(hTemp - parameters.getHmF2())));
423         final double[] arguments = new double[3];
424         arguments[0] = (hTemp - parameters.getHmF2()) / bf2;
425         arguments[1] = ((hTemp - parameters.getHmF1()) / bf1) * exp;
426         arguments[2] = ((hTemp - parameters.getHmE()) / be) * exp;
427 
428         // S coefficients
429         final double[] s = new double[3];
430         // Array of corrective terms
431         final double[] ds = new double[3];
432 
433         // Layer amplitudes
434         final double[] amplitudes = parameters.getLayerAmplitudes();
435 
436         // Fill arrays (Eq. 114 to 118)
437         for (int i = 0; i < 3; i++) {
438             if (FastMath.abs(arguments[i]) > 25.0) {
439                 s[i]  = 0.0;
440                 ds[i] = 0.0;
441             } else {
442                 final double expA   = clipExp(arguments[i]);
443                 final double opExpA = 1.0 + expA;
444                 s[i]  = amplitudes[i] * (expA / (opExpA * opExpA));
445                 ds[i] = ct[i] * ((1.0 - expA) / (1.0 + expA));
446             }
447         }
448 
449         // Electron density
450         final double aNo = MathArrays.linearCombination(s[0], 1.0, s[1], 1.0, s[2], 1.0);
451         if (h >= 100) {
452             return aNo * DENSITY_FACTOR;
453         } else {
454             // Chapman parameters (Eq. 119 and 120)
455             final double bc = 1.0 - 10.0 * (MathArrays.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]) / aNo);
456             final double z  = 0.1 * (h - 100.0);
457             // Electron density (Eq. 121)
458             return aNo * clipExp(1.0 - bc * z - clipExp(-z)) * DENSITY_FACTOR;
459         }
460     }
461 
462     /**
463      * Computes the electron density of the bottomside.
464      * @param <T> type of the elements
465      * @param field field of the elements
466      * @param h height in km
467      * @param parameters NeQuick model parameters
468      * @return the electron density N in m-3
469      */
470     private <T extends CalculusFieldElement<T>> T bottomElectronDensity(final Field<T> field,
471                                                                     final T h,
472                                                                     final FieldNeQuickParameters<T> parameters) {
473 
474         // Zero and One
475         final T zero = field.getZero();
476         final T one  = field.getOne();
477 
478         // Select the relevant B parameter for the current height (Eq. 109 and 110)
479         final T be;
480         if (h.getReal() > parameters.getHmE().getReal()) {
481             be = parameters.getBETop();
482         } else {
483             be = parameters.getBEBot();
484         }
485         final T bf1;
486         if (h.getReal() > parameters.getHmF1().getReal()) {
487             bf1 = parameters.getB1Top();
488         } else {
489             bf1 = parameters.getB1Bot();
490         }
491         final T bf2 = parameters.getB2Bot();
492 
493         // Useful array of constants
494         final T[] ct = MathArrays.buildArray(field, 3);
495         ct[0] = bf2.reciprocal();
496         ct[1] = bf1.reciprocal();
497         ct[2] = be.reciprocal();
498 
499         // Compute the exponential argument for each layer (Eq. 111 to 113)
500         // If h < 100km we use h = 100km as recommended in the reference document
501         final T   hTemp = FastMath.max(zero.newInstance(100.0), h);
502         final T   exp   = clipExp(field, FastMath.abs(hTemp.subtract(parameters.getHmF2())).add(1.0).divide(10.0).reciprocal());
503         final T[] arguments = MathArrays.buildArray(field, 3);
504         arguments[0] = hTemp.subtract(parameters.getHmF2()).divide(bf2);
505         arguments[1] = hTemp.subtract(parameters.getHmF1()).divide(bf1).multiply(exp);
506         arguments[2] = hTemp.subtract(parameters.getHmE()).divide(be).multiply(exp);
507 
508         // S coefficients
509         final T[] s = MathArrays.buildArray(field, 3);
510         // Array of corrective terms
511         final T[] ds = MathArrays.buildArray(field, 3);
512 
513         // Layer amplitudes
514         final T[] amplitudes = parameters.getLayerAmplitudes();
515 
516         // Fill arrays (Eq. 114 to 118)
517         for (int i = 0; i < 3; i++) {
518             if (FastMath.abs(arguments[i]).getReal() > 25.0) {
519                 s[i]  = zero;
520                 ds[i] = zero;
521             } else {
522                 final T expA   = clipExp(field, arguments[i]);
523                 final T opExpA = expA.add(1.0);
524                 s[i]  = amplitudes[i].multiply(expA.divide(opExpA.multiply(opExpA)));
525                 ds[i] = ct[i].multiply(expA.negate().add(1.0).divide(expA.add(1.0)));
526             }
527         }
528 
529         // Electron density
530         final T aNo = one.linearCombination(s[0], one, s[1], one, s[2], one);
531         if (h.getReal() >= 100) {
532             return aNo.multiply(DENSITY_FACTOR);
533         } else {
534             // Chapman parameters (Eq. 119 and 120)
535             final T bc = s[0].multiply(ds[0]).add(one.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2])).divide(aNo).multiply(10.0).negate().add(1.0);
536             final T z  = h.subtract(100.0).multiply(0.1);
537             // Electron density (Eq. 121)
538             return aNo.multiply(clipExp(field, bc.multiply(z).add(clipExp(field, z.negate())).negate().add(1.0))).multiply(DENSITY_FACTOR);
539         }
540     }
541 
542     /**
543      * Computes the electron density of the topside.
544      * @param h height in km
545      * @param parameters NeQuick model parameters
546      * @return the electron density N in m-3
547      */
548     private double topElectronDensity(final double h, final NeQuickParameters parameters) {
549 
550         // Constant parameters (Eq. 122 and 123)
551         final double g = 0.125;
552         final double r = 100.0;
553 
554         // Arguments deltaH and z (Eq. 124 and 125)
555         final double deltaH = h - parameters.getHmF2();
556         final double z      = deltaH / (parameters.getH0() * (1.0 + (r * g * deltaH) / (r * parameters.getH0() + g * deltaH)));
557 
558         // Exponential (Eq. 126)
559         final double ee = clipExp(z);
560 
561         // Electron density (Eq. 127)
562         if (ee > 1.0e11) {
563             return (4.0 * parameters.getNmF2() / ee) * DENSITY_FACTOR;
564         } else {
565             final double opExpZ = 1.0 + ee;
566             return ((4.0 * parameters.getNmF2() * ee) / (opExpZ * opExpZ)) * DENSITY_FACTOR;
567         }
568     }
569 
570     /**
571      * Computes the electron density of the topside.
572      * @param <T> type of the elements
573      * @param field field of the elements
574      * @param h height in km
575      * @param parameters NeQuick model parameters
576      * @return the electron density N in m-3
577      */
578     private <T extends CalculusFieldElement<T>> T topElectronDensity(final Field<T> field,
579                                                                  final T h,
580                                                                  final FieldNeQuickParameters<T> parameters) {
581 
582         // Constant parameters (Eq. 122 and 123)
583         final double g = 0.125;
584         final double r = 100.0;
585 
586         // Arguments deltaH and z (Eq. 124 and 125)
587         final T deltaH = h.subtract(parameters.getHmF2());
588         final T z      = deltaH.divide(parameters.getH0().multiply(deltaH.multiply(r).multiply(g).divide(parameters.getH0().multiply(r).add(deltaH.multiply(g))).add(1.0)));
589 
590         // Exponential (Eq. 126)
591         final T ee = clipExp(field, z);
592 
593         // Electron density (Eq. 127)
594         if (ee.getReal() > 1.0e11) {
595             return parameters.getNmF2().multiply(4.0).divide(ee).multiply(DENSITY_FACTOR);
596         } else {
597             final T opExpZ = ee.add(field.getOne());
598             return parameters.getNmF2().multiply(4.0).multiply(ee).divide(opExpZ.multiply(opExpZ)).multiply(DENSITY_FACTOR);
599         }
600     }
601 
602     /**
603      * Lazy loading of CCIR data.
604      * @param date current date components
605      */
606     private void loadsIfNeeded(final DateComponents date) {
607 
608         // Current month
609         final int currentMonth = date.getMonth();
610 
611         // Check if date have changed or if f2 and fm3 arrays are null
612         if (currentMonth != month || f2 == null || fm3 == null) {
613             this.month = currentMonth;
614 
615             // Read file
616             final CCIRLoader loader = new CCIRLoader();
617             loader.loadCCIRCoefficients(date);
618 
619             // Update arrays
620             this.f2 = loader.getF2();
621             this.fm3 = loader.getFm3();
622         }
623     }
624 
625     /**
626      * A clipped exponential function.
627      * <p>
628      * This function, describe in section F.2.12.2 of the reference document, is
629      * recommanded for the computation of exponential values.
630      * </p>
631      * @param power power for exponential function
632      * @return clipped exponential value
633      */
634     private double clipExp(final double power) {
635         if (power > 80.0) {
636             return 5.5406E34;
637         } else if (power < -80) {
638             return 1.8049E-35;
639         } else {
640             return FastMath.exp(power);
641         }
642     }
643 
644     /**
645      * A clipped exponential function.
646      * <p>
647      * This function, describe in section F.2.12.2 of the reference document, is
648      * recommanded for the computation of exponential values.
649      * </p>
650      * @param <T> type of the elements
651      * @param field field of the elements
652      * @param power power for exponential function
653      * @return clipped exponential value
654      */
655     private <T extends CalculusFieldElement<T>> T clipExp(final Field<T> field, final T power) {
656         final T zero = field.getZero();
657         if (power.getReal() > 80.0) {
658             return zero.newInstance(5.5406E34);
659         } else if (power.getReal() < -80) {
660             return zero.newInstance(1.8049E-35);
661         } else {
662             return FastMath.exp(power);
663         }
664     }
665 
666     /** Get a data stream.
667      * @param name file name of the resource stream
668      * @return stream
669      */
670     private static InputStream getStream(final String name) {
671         return NeQuickModel.class.getResourceAsStream(name);
672     }
673 
674     /**
675      * Parser for Modified Dip Latitude (MODIP) grid file.
676      * <p>
677      * The MODIP grid allows to estimate MODIP μ [deg] at a given point (φ,λ)
678      * by interpolation of the relevant values contained in the support file.
679      * </p> <p>
680      * The file contains the values of MODIP (expressed in degrees) on a geocentric grid
681      * from 90°S to 90°N with a 5-degree step in latitude and from 180°W to 180°E with a
682      * 10-degree in longitude.
683      * </p>
684      */
685     private static class MODIPLoader {
686 
687         /** Supported name for MODIP grid. */
688         private static final String SUPPORTED_NAME = NEQUICK_BASE + "modip.txt";
689 
690         /** MODIP grid. */
691         private double[][] grid;
692 
693         /**
694          * Build a new instance.
695          */
696         MODIPLoader() {
697             this.grid = null;
698         }
699 
700         /** Returns the MODIP grid array.
701          * @return the MODIP grid array
702          */
703         public double[][] getMODIPGrid() {
704             return grid.clone();
705         }
706 
707         /**
708          * Load the data using supported names.
709          */
710         public void loadMODIPGrid() {
711             try (InputStream in = getStream(SUPPORTED_NAME)) {
712                 loadData(in, SUPPORTED_NAME);
713             } catch (IOException e) {
714                 throw new OrekitException(OrekitMessages.INTERNAL_ERROR, e);
715             }
716 
717             // Throw an exception if MODIP grid was not loaded properly
718             if (grid == null) {
719                 throw new OrekitException(OrekitMessages.MODIP_GRID_NOT_LOADED, SUPPORTED_NAME);
720             }
721         }
722 
723         /**
724          * Load data from a stream.
725          * @param input input stream
726          * @param name name of the file
727          * @throws IOException if data can't be read
728          */
729         public void loadData(final InputStream input, final String name)
730             throws IOException {
731 
732             // Grid size
733             final int size = 39;
734 
735             // Initialize array
736             final double[][] array = new double[size][size];
737 
738             // Open stream and parse data
739             int   lineNumber = 0;
740             String line      = null;
741             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
742                  BufferedReader    br = new BufferedReader(isr)) {
743 
744                 for (line = br.readLine(); line != null; line = br.readLine()) {
745                     ++lineNumber;
746                     line = line.trim();
747 
748                     // Read grid data
749                     if (line.length() > 0) {
750                         final String[] modip_line = SEPARATOR.split(line);
751                         for (int column = 0; column < modip_line.length; column++) {
752                             array[lineNumber - 1][column] = Double.parseDouble(modip_line[column]);
753                         }
754                     }
755 
756                 }
757 
758             } catch (NumberFormatException nfe) {
759                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
760                                           lineNumber, name, line);
761             }
762 
763             // Clone parsed grid
764             grid = array.clone();
765 
766         }
767     }
768 
769     /**
770      * Parser for CCIR files.
771      * <p>
772      * Numerical grid maps which describe the regular variation of the ionosphere.
773      * They are used to derive other variables such as critical frequencies and transmission factors.
774      * </p> <p>
775      * The coefficients correspond to low and high solar activity conditions.
776      * </p> <p>
777      * The CCIR file naming convention is ccirXX.asc where each XX means month + 10.
778      * </p> <p>
779      * Coefficients are store into tow arrays, F2 and Fm3. F2 coefficients are used for the computation
780      * of the F2 layer critical frequency. Fm3 for the computation of the F2 layer maximum usable frequency factor.
781      * The size of these two arrays is fixed and discussed into the section 2.5.3.2
782      * of the reference document.
783      * </p>
784      */
785     private static class CCIRLoader {
786 
787         /** Default supported files name pattern. */
788         public static final String DEFAULT_SUPPORTED_NAME = "ccir**.asc";
789 
790         /** Total number of F2 coefficients contained in the file. */
791         private static final int NUMBER_F2_COEFFICIENTS = 1976;
792 
793         /** Rows number for F2 and Fm3 arrays. */
794         private static final int ROWS = 2;
795 
796         /** Columns number for F2 array. */
797         private static final int TOTAL_COLUMNS_F2 = 76;
798 
799         /** Columns number for Fm3 array. */
800         private static final int TOTAL_COLUMNS_FM3 = 49;
801 
802         /** Depth of F2 array. */
803         private static final int DEPTH_F2 = 13;
804 
805         /** Depth of Fm3 array. */
806         private static final int DEPTH_FM3 = 9;
807 
808         /** Regular expression for supported file name. */
809         private String supportedName;
810 
811         /** F2 coefficients used for the computation of the F2 layer critical frequency. */
812         private double[][][] f2Loader;
813 
814         /** Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor. */
815         private double[][][] fm3Loader;
816 
817         /**
818          * Build a new instance.
819          */
820         CCIRLoader() {
821             this.supportedName = DEFAULT_SUPPORTED_NAME;
822             this.f2Loader  = null;
823             this.fm3Loader = null;
824         }
825 
826         /**
827          * Get the F2 coefficients used for the computation of the F2 layer critical frequency.
828          * @return the F2 coefficients
829          */
830         public double[][][] getF2() {
831             return f2Loader.clone();
832         }
833 
834         /**
835          * Get the Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor.
836          * @return the F2 coefficients
837          */
838         public double[][][] getFm3() {
839             return fm3Loader.clone();
840         }
841 
842         /** Load the data for a given month.
843          * @param dateComponents month given but its DateComponents
844          */
845         public void loadCCIRCoefficients(final DateComponents dateComponents) {
846 
847             // The files are named ccirXX.asc where XX substitute the month of the year + 10
848             final int currentMonth = dateComponents.getMonth();
849             this.supportedName = NEQUICK_BASE + String.format("ccir%02d.asc", currentMonth + 10);
850             try (InputStream in = getStream(supportedName)) {
851                 loadData(in, supportedName);
852             } catch (IOException e) {
853                 throw new OrekitException(OrekitMessages.INTERNAL_ERROR, e);
854             }
855             // Throw an exception if F2 or Fm3 were not loaded properly
856             if (f2Loader == null || fm3Loader == null) {
857                 throw new OrekitException(OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, supportedName);
858             }
859 
860         }
861 
862         /**
863          * Load data from a stream.
864          * @param input input stream
865          * @param name name of the file
866          * @throws IOException if data can't be read
867          */
868         public void loadData(final InputStream input, final String name)
869             throws IOException {
870 
871             // Initialize arrays
872             final double[][][] f2Temp  = new double[ROWS][TOTAL_COLUMNS_F2][DEPTH_F2];
873             final double[][][] fm3Temp = new double[ROWS][TOTAL_COLUMNS_FM3][DEPTH_FM3];
874 
875             // Placeholders for parsed data
876             int    lineNumber       = 0;
877             int    index            = 0;
878             int    currentRowF2     = 0;
879             int    currentColumnF2  = 0;
880             int    currentDepthF2   = 0;
881             int    currentRowFm3    = 0;
882             int    currentColumnFm3 = 0;
883             int    currentDepthFm3  = 0;
884             String line             = null;
885 
886             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
887                  BufferedReader    br = new BufferedReader(isr)) {
888 
889                 for (line = br.readLine(); line != null; line = br.readLine()) {
890                     ++lineNumber;
891                     line = line.trim();
892 
893                     // Read grid data
894                     if (line.length() > 0) {
895                         final String[] ccir_line = SEPARATOR.split(line);
896                         for (int i = 0; i < ccir_line.length; i++) {
897 
898                             if (index < NUMBER_F2_COEFFICIENTS) {
899                                 // Parse F2 coefficients
900                                 if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 < (TOTAL_COLUMNS_F2 - 1)) {
901                                     currentDepthF2 = 0;
902                                     currentColumnF2++;
903                                 } else if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 >= (TOTAL_COLUMNS_F2 - 1)) {
904                                     currentDepthF2 = 0;
905                                     currentColumnF2 = 0;
906                                     currentRowF2++;
907                                 }
908                                 f2Temp[currentRowF2][currentColumnF2][currentDepthF2++] = Double.parseDouble(ccir_line[i]);
909                                 index++;
910                             } else {
911                                 // Parse Fm3 coefficients
912                                 if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 < (TOTAL_COLUMNS_FM3 - 1)) {
913                                     currentDepthFm3 = 0;
914                                     currentColumnFm3++;
915                                 } else if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 >= (TOTAL_COLUMNS_FM3 - 1)) {
916                                     currentDepthFm3 = 0;
917                                     currentColumnFm3 = 0;
918                                     currentRowFm3++;
919                                 }
920                                 fm3Temp[currentRowFm3][currentColumnFm3][currentDepthFm3++] = Double.parseDouble(ccir_line[i]);
921                                 index++;
922                             }
923 
924                         }
925                     }
926 
927                 }
928 
929             } catch (NumberFormatException nfe) {
930                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
931                                           lineNumber, name, line);
932             }
933 
934             f2Loader  = f2Temp.clone();
935             fm3Loader = fm3Temp.clone();
936 
937         }
938 
939     }
940 
941     /**
942      * Container for ray-perigee parameters.
943      * By convention, point 1 is at lower height.
944      */
945     private static class Ray {
946 
947         /** Threshold for ray-perigee parameters computation. */
948         private static final double THRESHOLD = 1.0e-10;
949 
950         /** Distance of the first point from the ray perigee [m]. */
951         private final double s1;
952 
953         /** Distance of the second point from the ray perigee [m]. */
954         private final double s2;
955 
956         /** Ray-perigee radius [m]. */
957         private final double rp;
958 
959         /** Ray-perigee latitude [rad]. */
960         private final double latP;
961 
962         /** Ray-perigee longitude [rad]. */
963         private final double lonP;
964 
965         /** Sine and cosine of ray-perigee latitude. */
966         private final SinCos scLatP;
967 
968         /** Sine of azimuth of satellite as seen from ray-perigee. */
969         private final double sinAzP;
970 
971         /** Cosine of azimuth of satellite as seen from ray-perigee. */
972         private final double cosAzP;
973 
974         /**
975          * Constructor.
976          * @param recP receiver position
977          * @param satP satellite position
978          */
979         Ray(final GeodeticPoint recP, final GeodeticPoint satP) {
980 
981             // Integration limits in meters (Eq. 140 and 141)
982             final double r1 = RE + recP.getAltitude();
983             final double r2 = RE + satP.getAltitude();
984 
985             // Useful parameters
986             final double lat1     = recP.getLatitude();
987             final double lat2     = satP.getLatitude();
988             final double lon1     = recP.getLongitude();
989             final double lon2     = satP.getLongitude();
990             final SinCos scLatSat = FastMath.sinCos(lat2);
991             final SinCos scLatRec = FastMath.sinCos(lat1);
992             final SinCos scLon21  = FastMath.sinCos(lon2 - lon1);
993 
994             // Zenith angle computation (Eq. 153 to 155)
995             final double cosD = scLatRec.sin() * scLatSat.sin() +
996                                 scLatRec.cos() * scLatSat.cos() * scLon21.cos();
997             final double sinD = FastMath.sqrt(1.0 - cosD * cosD);
998             final double z = FastMath.atan2(sinD, cosD - (r1 / r2));
999 
1000             // Ray-perigee computation in meters (Eq. 156)
1001             this.rp = r1 * FastMath.sin(z);
1002 
1003             // Ray-perigee latitude and longitude
1004             if (FastMath.abs(FastMath.abs(lat1) - 0.5 * FastMath.PI) < THRESHOLD) {
1005 
1006                 // Ray-perigee latitude (Eq. 157)
1007                 if (lat1 < 0) {
1008                     this.latP = -z;
1009                 } else {
1010                     this.latP = z;
1011                 }
1012 
1013                 // Ray-perigee longitude (Eq. 164)
1014                 if (z < 0) {
1015                     this.lonP = lon2;
1016                 } else {
1017                     this.lonP = lon2 + FastMath.PI;
1018                 }
1019 
1020             } else {
1021 
1022                 // Ray-perigee latitude (Eq. 158 to 163)
1023                 final double deltaP   = 0.5 * FastMath.PI - z;
1024                 final SinCos scDeltaP = FastMath.sinCos(deltaP);
1025                 final double sinAz    = scLon21.sin() * scLatSat.cos() / sinD;
1026                 final double cosAz    = (scLatSat.sin() - cosD * scLatRec.sin()) / (sinD * scLatRec.cos());
1027                 final double sinLatP  = scLatRec.sin() * scDeltaP.cos() - scLatRec.cos() * scDeltaP.sin() * cosAz;
1028                 final double cosLatP  = FastMath.sqrt(1.0 - sinLatP * sinLatP);
1029                 this.latP = FastMath.atan2(sinLatP, cosLatP);
1030 
1031                 // Ray-perigee longitude (Eq. 165 to 167)
1032                 final double sinLonP = -sinAz * scDeltaP.sin() / cosLatP;
1033                 final double cosLonP = (scDeltaP.cos() - scLatRec.sin() * sinLatP) / (scLatRec.cos() * cosLatP);
1034                 this.lonP = FastMath.atan2(sinLonP, cosLonP) + lon1;
1035 
1036             }
1037 
1038             // Sine and cosine of ray-perigee latitude
1039             this.scLatP = FastMath.sinCos(latP);
1040 
1041             final SinCos scLon = FastMath.sinCos(lon2 - lonP);
1042             // Sine and cosine of azimuth of satellite as seen from ray-perigee
1043             final double psi   = greatCircleAngle(scLatSat, scLon);
1044             final SinCos scPsi = FastMath.sinCos(psi);
1045             if (FastMath.abs(FastMath.abs(latP) - 0.5 * FastMath.PI) < THRESHOLD) {
1046                 // Eq. 172 and 173
1047                 this.sinAzP = 0.0;
1048                 if (latP < 0.0) {
1049                     this.cosAzP = 1;
1050                 } else {
1051                     this.cosAzP = -1;
1052                 }
1053             } else {
1054                 // Eq. 174 and 175
1055                 this.sinAzP =  scLatSat.cos() * scLon.sin()                 /  scPsi.sin();
1056                 this.cosAzP = (scLatSat.sin() - scLatP.sin() * scPsi.cos()) / (scLatP.cos() * scPsi.sin());
1057             }
1058 
1059             // Integration en points s1 and s2 in meters (Eq. 176 and 177)
1060             this.s1 = FastMath.sqrt(r1 * r1 - rp * rp);
1061             this.s2 = FastMath.sqrt(r2 * r2 - rp * rp);
1062         }
1063 
1064         /**
1065          * Get the distance of the first point from the ray perigee.
1066          * @return s1 in meters
1067          */
1068         public double getS1() {
1069             return s1;
1070         }
1071 
1072         /**
1073          * Get the distance of the second point from the ray perigee.
1074          * @return s2 in meters
1075          */
1076         public double getS2() {
1077             return s2;
1078         }
1079 
1080         /**
1081          * Get the ray-perigee radius.
1082          * @return the ray-perigee radius in meters
1083          */
1084         public double getRadius() {
1085             return rp;
1086         }
1087 
1088         /**
1089          * Get the ray-perigee latitude.
1090          * @return the ray-perigee latitude in radians
1091          */
1092         public double getLatitude() {
1093             return latP;
1094         }
1095 
1096         /**
1097          * Get the ray-perigee longitude.
1098          * @return the ray-perigee longitude in radians
1099          */
1100         public double getLongitude() {
1101             return lonP;
1102         }
1103 
1104         /**
1105          * Get the sine of azimuth of satellite as seen from ray-perigee.
1106          * @return the sine of azimuth
1107          */
1108         public double getSineAz() {
1109             return sinAzP;
1110         }
1111 
1112         /**
1113          * Get the cosine of azimuth of satellite as seen from ray-perigee.
1114          * @return the cosine of azimuth
1115          */
1116         public double getCosineAz() {
1117             return cosAzP;
1118         }
1119 
1120         /**
1121          * Compute the great circle angle from ray-perigee to satellite.
1122          * <p>
1123          * This method used the equations 168 to 171 pf the reference document.
1124          * </p>
1125          * @param scLat sine and cosine of satellite latitude
1126          * @param scLon sine and cosine of satellite longitude minus receiver longitude
1127          * @return the great circle angle in radians
1128          */
1129         private double greatCircleAngle(final SinCos scLat, final SinCos scLon) {
1130             if (FastMath.abs(FastMath.abs(latP) - 0.5 * FastMath.PI) < THRESHOLD) {
1131                 return FastMath.abs(FastMath.asin(scLat.sin()) - latP);
1132             } else {
1133                 final double cosPhi = scLatP.sin() * scLat.sin() +
1134                                       scLatP.cos() * scLat.cos() * scLon.cos();
1135                 final double sinPhi = FastMath.sqrt(1.0 - cosPhi * cosPhi);
1136                 return FastMath.atan2(sinPhi, cosPhi);
1137             }
1138         }
1139     }
1140 
1141     /**
1142      * Container for ray-perigee parameters.
1143      * By convention, point 1 is at lower height.
1144      */
1145     private static class FieldRay <T extends CalculusFieldElement<T>> {
1146 
1147         /** Threshold for ray-perigee parameters computation. */
1148         private static final double THRESHOLD = 1.0e-10;
1149 
1150         /** Distance of the first point from the ray perigee [m]. */
1151         private final T s1;
1152 
1153         /** Distance of the second point from the ray perigee [m]. */
1154         private final T s2;
1155 
1156         /** Ray-perigee radius [m]. */
1157         private final T rp;
1158 
1159         /** Ray-perigee latitude [rad]. */
1160         private final T latP;
1161 
1162         /** Ray-perigee longitude [rad]. */
1163         private final T lonP;
1164 
1165         /** Sine and cosine of ray-perigee latitude. */
1166         private final FieldSinCos<T> scLatP;
1167 
1168         /** Sine of azimuth of satellite as seen from ray-perigee. */
1169         private final T sinAzP;
1170 
1171         /** Cosine of azimuth of satellite as seen from ray-perigee. */
1172         private final T cosAzP;
1173 
1174         /**
1175          * Constructor.
1176          * @param field field of the elements
1177          * @param recP receiver position
1178          * @param satP satellite position
1179          */
1180         FieldRay(final Field<T> field, final FieldGeodeticPoint<T> recP, final FieldGeodeticPoint<T> satP) {
1181 
1182             // Integration limits in meters (Eq. 140 and 141)
1183             final T r1 = recP.getAltitude().add(RE);
1184             final T r2 = satP.getAltitude().add(RE);
1185 
1186             // Useful parameters
1187             final T pi   = r1.getPi();
1188             final T lat1 = recP.getLatitude();
1189             final T lat2 = satP.getLatitude();
1190             final T lon1 = recP.getLongitude();
1191             final T lon2 = satP.getLongitude();
1192             final FieldSinCos<T> scLatSat = FastMath.sinCos(lat2);
1193             final FieldSinCos<T> scLatRec = FastMath.sinCos(lat1);
1194 
1195             // Zenith angle computation (Eq. 153 to 155)
1196             final T cosD = scLatRec.sin().multiply(scLatSat.sin()).
1197                             add(scLatRec.cos().multiply(scLatSat.cos()).multiply(FastMath.cos(lon2.subtract(lon1))));
1198             final T sinD = FastMath.sqrt(cosD.multiply(cosD).negate().add(1.0));
1199             final T z = FastMath.atan2(sinD, cosD.subtract(r1.divide(r2)));
1200 
1201             // Ray-perigee computation in meters (Eq. 156)
1202             this.rp = r1.multiply(FastMath.sin(z));
1203 
1204             // Ray-perigee latitude and longitude
1205             if (FastMath.abs(FastMath.abs(lat1).subtract(pi.multiply(0.5)).getReal()) < THRESHOLD) {
1206 
1207                 // Ray-perigee latitude (Eq. 157)
1208                 if (lat1.getReal() < 0) {
1209                     this.latP = z.negate();
1210                 } else {
1211                     this.latP = z;
1212                 }
1213 
1214                 // Ray-perigee longitude (Eq. 164)
1215                 if (z.getReal() < 0) {
1216                     this.lonP = lon2;
1217                 } else {
1218                     this.lonP = lon2.add(pi);
1219                 }
1220 
1221             } else {
1222 
1223                 // Ray-perigee latitude (Eq. 158 to 163)
1224                 final T deltaP = z.negate().add(pi.multiply(0.5));
1225                 final FieldSinCos<T> scDeltaP = FastMath.sinCos(deltaP);
1226                 final T sinAz    = FastMath.sin(lon2.subtract(lon1)).multiply(scLatSat.cos()).divide(sinD);
1227                 final T cosAz    = scLatSat.sin().subtract(cosD.multiply(scLatRec.sin())).divide(sinD.multiply(scLatRec.cos()));
1228                 final T sinLatP  = scLatRec.sin().multiply(scDeltaP.cos()).subtract(scLatRec.cos().multiply(scDeltaP.sin()).multiply(cosAz));
1229                 final T cosLatP  = FastMath.sqrt(sinLatP.multiply(sinLatP).negate().add(1.0));
1230                 this.latP = FastMath.atan2(sinLatP, cosLatP);
1231 
1232                 // Ray-perigee longitude (Eq. 165 to 167)
1233                 final T sinLonP = sinAz.negate().multiply(scDeltaP.sin()).divide(cosLatP);
1234                 final T cosLonP = scDeltaP.cos().subtract(scLatRec.sin().multiply(sinLatP)).divide(scLatRec.cos().multiply(cosLatP));
1235                 this.lonP = FastMath.atan2(sinLonP, cosLonP).add(lon1);
1236 
1237             }
1238 
1239             // Sine and cosine of ray-perigee latitude
1240             this.scLatP = FastMath.sinCos(latP);
1241 
1242             final FieldSinCos<T> scLon = FastMath.sinCos(lon2.subtract(lonP));
1243             // Sine and cosie of azimuth of satellite as seen from ray-perigee
1244             final T psi = greatCircleAngle(scLatSat, scLon);
1245             final FieldSinCos<T> scPsi = FastMath.sinCos(psi);
1246             if (FastMath.abs(FastMath.abs(latP).subtract(pi.multiply(0.5)).getReal()) < THRESHOLD) {
1247                 // Eq. 172 and 173
1248                 this.sinAzP = field.getZero();
1249                 if (latP.getReal() < 0.0) {
1250                     this.cosAzP = field.getOne();
1251                 } else {
1252                     this.cosAzP = field.getOne().negate();
1253                 }
1254             } else {
1255                 // Eq. 174 and 175
1256                 this.sinAzP = scLatSat.cos().multiply(scLon.sin()).divide(scPsi.sin());
1257                 this.cosAzP = scLatSat.sin().subtract(scLatP.sin().multiply(scPsi.cos())).divide(scLatP.cos().multiply(scPsi.sin()));
1258             }
1259 
1260             // Integration en points s1 and s2 in meters (Eq. 176 and 177)
1261             this.s1 = FastMath.sqrt(r1.multiply(r1).subtract(rp.multiply(rp)));
1262             this.s2 = FastMath.sqrt(r2.multiply(r2).subtract(rp.multiply(rp)));
1263         }
1264 
1265         /**
1266          * Get the distance of the first point from the ray perigee.
1267          * @return s1 in meters
1268          */
1269         public T getS1() {
1270             return s1;
1271         }
1272 
1273         /**
1274          * Get the distance of the second point from the ray perigee.
1275          * @return s2 in meters
1276          */
1277         public T getS2() {
1278             return s2;
1279         }
1280 
1281         /**
1282          * Get the ray-perigee radius.
1283          * @return the ray-perigee radius in meters
1284          */
1285         public T getRadius() {
1286             return rp;
1287         }
1288 
1289         /**
1290          * Get the ray-perigee latitude.
1291          * @return the ray-perigee latitude in radians
1292          */
1293         public T getLatitude() {
1294             return latP;
1295         }
1296 
1297         /**
1298          * Get the ray-perigee longitude.
1299          * @return the ray-perigee longitude in radians
1300          */
1301         public T getLongitude() {
1302             return lonP;
1303         }
1304 
1305         /**
1306          * Get the sine of azimuth of satellite as seen from ray-perigee.
1307          * @return the sine of azimuth
1308          */
1309         public T getSineAz() {
1310             return sinAzP;
1311         }
1312 
1313         /**
1314          * Get the cosine of azimuth of satellite as seen from ray-perigee.
1315          * @return the cosine of azimuth
1316          */
1317         public T getCosineAz() {
1318             return cosAzP;
1319         }
1320 
1321         /**
1322          * Compute the great circle angle from ray-perigee to satellite.
1323          * <p>
1324          * This method used the equations 168 to 171 pf the reference document.
1325          * </p>
1326          * @param scLat sine and cosine of satellite latitude
1327          * @param scLon sine and cosine of satellite longitude minus receiver longitude
1328          * @return the great circle angle in radians
1329          */
1330         private T greatCircleAngle(final FieldSinCos<T> scLat, final FieldSinCos<T> scLon) {
1331             if (FastMath.abs(FastMath.abs(latP).getReal() - 0.5 * FastMath.PI) < THRESHOLD) {
1332                 return FastMath.abs(FastMath.asin(scLat.sin()).subtract(latP));
1333             } else {
1334                 final T cosPhi = scLatP.sin().multiply(scLat.sin()).add(
1335                                  scLatP.cos().multiply(scLat.cos()).multiply(scLon.cos()));
1336                 final T sinPhi = FastMath.sqrt(cosPhi.multiply(cosPhi).negate().add(1.0));
1337                 return FastMath.atan2(sinPhi, cosPhi);
1338             }
1339         }
1340     }
1341 
1342     /** Performs the computation of the coordinates along the integration path. */
1343     private static class Segment {
1344 
1345         /** Latitudes [rad]. */
1346         private final double[] latitudes;
1347 
1348         /** Longitudes [rad]. */
1349         private final double[] longitudes;
1350 
1351         /** Heights [m]. */
1352         private final double[] heights;
1353 
1354         /** Integration step [m]. */
1355         private final double deltaN;
1356 
1357         /**
1358          * Constructor.
1359          * @param n number of points used for the integration
1360          * @param ray ray-perigee parameters
1361          */
1362         Segment(final int n, final Ray ray) {
1363             // Integration en points
1364             final double s1 = ray.getS1();
1365             final double s2 = ray.getS2();
1366 
1367             // Integration step (Eq. 195)
1368             this.deltaN = (s2 - s1) / n;
1369 
1370             // Segments
1371             final double[] s = getSegments(n, s1, s2);
1372 
1373             // Useful parameters
1374             final double rp = ray.getRadius();
1375             final SinCos scLatP = FastMath.sinCos(ray.getLatitude());
1376 
1377             // Geodetic coordinates
1378             final int size = s.length;
1379             heights    = new double[size];
1380             latitudes  = new double[size];
1381             longitudes = new double[size];
1382             for (int i = 0; i < size; i++) {
1383                 // Heights (Eq. 178)
1384                 heights[i] = FastMath.sqrt(s[i] * s[i] + rp * rp) - RE;
1385 
1386                 // Great circle parameters (Eq. 179 to 181)
1387                 final double tanDs = s[i] / rp;
1388                 final double cosDs = 1.0 / FastMath.sqrt(1.0 + tanDs * tanDs);
1389                 final double sinDs = tanDs * cosDs;
1390 
1391                 // Latitude (Eq. 182 to 183)
1392                 final double sinLatS = scLatP.sin() * cosDs + scLatP.cos() * sinDs * ray.getCosineAz();
1393                 final double cosLatS = FastMath.sqrt(1.0 - sinLatS * sinLatS);
1394                 latitudes[i] = FastMath.atan2(sinLatS, cosLatS);
1395 
1396                 // Longitude (Eq. 184 to 187)
1397                 final double sinLonS = sinDs * ray.getSineAz() * scLatP.cos();
1398                 final double cosLonS = cosDs - scLatP.sin() * sinLatS;
1399                 longitudes[i] = FastMath.atan2(sinLonS, cosLonS) + ray.getLongitude();
1400             }
1401         }
1402 
1403         /**
1404          * Computes the distance of a point from the ray-perigee.
1405          * @param n number of points used for the integration
1406          * @param s1 lower boundary
1407          * @param s2 upper boundary
1408          * @return the distance of a point from the ray-perigee in km
1409          */
1410         private double[] getSegments(final int n, final double s1, final double s2) {
1411             // Eq. 196
1412             final double g = 0.5773502691896 * deltaN;
1413             // Eq. 197
1414             final double y = s1 + (deltaN - g) * 0.5;
1415             final double[] segments = new double[2 * n];
1416             int index = 0;
1417             for (int i = 0; i < n; i++) {
1418                 // Eq. 198
1419                 segments[index] = y + i * deltaN;
1420                 index++;
1421                 segments[index] = y + i * deltaN + g;
1422                 index++;
1423             }
1424             return segments;
1425         }
1426 
1427         /**
1428          * Get the latitudes of the coordinates along the integration path.
1429          * @return the latitudes in radians
1430          */
1431         public double[] getLatitudes() {
1432             return latitudes;
1433         }
1434 
1435         /**
1436          * Get the longitudes of the coordinates along the integration path.
1437          * @return the longitudes in radians
1438          */
1439         public double[] getLongitudes() {
1440             return longitudes;
1441         }
1442 
1443         /**
1444          * Get the heights of the coordinates along the integration path.
1445          * @return the heights in m
1446          */
1447         public double[] getHeights() {
1448             return heights;
1449         }
1450 
1451         /**
1452          * Get the integration step.
1453          * @return the integration step in meters
1454          */
1455         public double getInterval() {
1456             return deltaN;
1457         }
1458     }
1459 
1460     /** Performs the computation of the coordinates along the integration path. */
1461     private static class FieldSegment <T extends CalculusFieldElement<T>> {
1462 
1463         /** Latitudes [rad]. */
1464         private final T[] latitudes;
1465 
1466         /** Longitudes [rad]. */
1467         private final T[] longitudes;
1468 
1469         /** Heights [m]. */
1470         private final T[] heights;
1471 
1472         /** Integration step [m]. */
1473         private final T deltaN;
1474 
1475         /**
1476          * Constructor.
1477          * @param field field of the elements
1478          * @param n number of points used for the integration
1479          * @param ray ray-perigee parameters
1480          */
1481         FieldSegment(final Field<T> field, final int n, final FieldRay<T> ray) {
1482             // Integration en points
1483             final T s1 = ray.getS1();
1484             final T s2 = ray.getS2();
1485 
1486             // Integration step (Eq. 195)
1487             this.deltaN = s2.subtract(s1).divide(n);
1488 
1489             // Segments
1490             final T[] s = getSegments(field, n, s1, s2);
1491 
1492             // Useful parameters
1493             final T rp = ray.getRadius();
1494             final FieldSinCos<T> scLatP = FastMath.sinCos(ray.getLatitude());
1495 
1496             // Geodetic coordinates
1497             final int size = s.length;
1498             heights    = MathArrays.buildArray(field, size);
1499             latitudes  = MathArrays.buildArray(field, size);
1500             longitudes = MathArrays.buildArray(field, size);
1501             for (int i = 0; i < size; i++) {
1502                 // Heights (Eq. 178)
1503                 heights[i] = FastMath.sqrt(s[i].multiply(s[i]).add(rp.multiply(rp))).subtract(RE);
1504 
1505                 // Great circle parameters (Eq. 179 to 181)
1506                 final T tanDs = s[i].divide(rp);
1507                 final T cosDs = FastMath.sqrt(tanDs.multiply(tanDs).add(1.0)).reciprocal();
1508                 final T sinDs = tanDs.multiply(cosDs);
1509 
1510                 // Latitude (Eq. 182 to 183)
1511                 final T sinLatS = scLatP.sin().multiply(cosDs).add(scLatP.cos().multiply(sinDs).multiply(ray.getCosineAz()));
1512                 final T cosLatS = FastMath.sqrt(sinLatS.multiply(sinLatS).negate().add(1.0));
1513                 latitudes[i] = FastMath.atan2(sinLatS, cosLatS);
1514 
1515                 // Longitude (Eq. 184 to 187)
1516                 final T sinLonS = sinDs.multiply(ray.getSineAz()).multiply(scLatP.cos());
1517                 final T cosLonS = cosDs.subtract(scLatP.sin().multiply(sinLatS));
1518                 longitudes[i] = FastMath.atan2(sinLonS, cosLonS).add(ray.getLongitude());
1519             }
1520         }
1521 
1522         /**
1523          * Computes the distance of a point from the ray-perigee.
1524          * @param field field of the elements
1525          * @param n number of points used for the integration
1526          * @param s1 lower boundary
1527          * @param s2 upper boundary
1528          * @return the distance of a point from the ray-perigee in km
1529          */
1530         private T[] getSegments(final Field<T> field, final int n, final T s1, final T s2) {
1531             // Eq. 196
1532             final T g = deltaN.multiply(0.5773502691896);
1533             // Eq. 197
1534             final T y = s1.add(deltaN.subtract(g).multiply(0.5));
1535             final T[] segments = MathArrays.buildArray(field, 2 * n);
1536             int index = 0;
1537             for (int i = 0; i < n; i++) {
1538                 // Eq. 198
1539                 segments[index] = y.add(deltaN.multiply(i));
1540                 index++;
1541                 segments[index] = y.add(deltaN.multiply(i)).add(g);
1542                 index++;
1543             }
1544             return segments;
1545         }
1546 
1547         /**
1548          * Get the latitudes of the coordinates along the integration path.
1549          * @return the latitudes in radians
1550          */
1551         public T[] getLatitudes() {
1552             return latitudes;
1553         }
1554 
1555         /**
1556          * Get the longitudes of the coordinates along the integration path.
1557          * @return the longitudes in radians
1558          */
1559         public T[] getLongitudes() {
1560             return longitudes;
1561         }
1562 
1563         /**
1564          * Get the heights of the coordinates along the integration path.
1565          * @return the heights in m
1566          */
1567         public T[] getHeights() {
1568             return heights;
1569         }
1570 
1571         /**
1572          * Get the integration step.
1573          * @return the integration step in meters
1574          */
1575         public T getInterval() {
1576             return deltaN;
1577         }
1578     }
1579 
1580 }