1 /* Copyright 2002-2025 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.atmosphere.data;
18
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.stream.Collectors;
22
23 import org.hipparchus.analysis.UnivariateFunction;
24 import org.hipparchus.analysis.interpolation.LinearInterpolator;
25 import org.hipparchus.util.FastMath;
26 import org.orekit.annotation.DefaultDataContext;
27 import org.orekit.data.DataContext;
28 import org.orekit.data.DataProvidersManager;
29 import org.orekit.data.DataSource;
30 import org.orekit.models.earth.atmosphere.data.MarshallSolarActivityFutureEstimationLoader.LineParameters;
31 import org.orekit.time.AbsoluteDate;
32 import org.orekit.time.DateComponents;
33 import org.orekit.time.DateTimeComponents;
34 import org.orekit.time.TimeComponents;
35 import org.orekit.time.TimeScale;
36 import org.orekit.time.TimeStampedDouble;
37 import org.orekit.utils.Constants;
38 import org.orekit.utils.GenericTimeStampedCache;
39 import org.orekit.utils.OrekitConfiguration;
40 import org.orekit.utils.TimeStampedGenerator;
41
42 /**
43 * This class provides solar activity data needed by atmospheric models: F107 solar flux, Ap and Kp indexes.
44 * <p>
45 * Data comes from the NASA Marshall Solar Activity Future Estimation (MSAFE) as estimates of monthly F10.7
46 * Mean solar flux and Ap geomagnetic parameter
47 * (see <a href="https://www.nasa.gov/solar-cycle-progression-and-forecast"> Marshall Solar Activity website</a>).
48 *
49 * <p>Data can be retrieved at the NASA
50 * <a href="https://www.nasa.gov/solar-cycle-progression-and-forecast/archived-forecast/"> Marshall Solar Activity archived forecast</a>.
51 * Here Kp indices are deduced from Ap indexes, which in turn are tabulated equivalent of retrieved Ap values.
52 *
53 * <p> If several MSAFE files are available, some dates may appear in several files (for example August 2007 is in all files from
54 * the first one published in March 1999 to the February 2008 file). In this case, the data from the most recent file is used
55 * and the older ones are discarded. The date of the file is assumed to be 6 months after its first entry (which explains why
56 * the file having August 2007 as its first entry is the February 2008 file). This implies that MSAFE files must <em>not</em>
57 * be edited to change their time span, otherwise this would break the old entries overriding mechanism.
58 *
59 * <p>With these data, the {@link #getInstantFlux(AbsoluteDate)} and {@link #getMeanFlux(AbsoluteDate)} methods return the same
60 * values and the {@link #get24HoursKp(AbsoluteDate)} and {@link #getThreeHourlyKP(AbsoluteDate)} methods return the same
61 * values.
62 *
63 * <p>Conversion from Ap index values in the MSAFE file to Kp values used by atmosphere models is done using Jacchia's equation
64 * in [1].
65 *
66 * <p>With these data, the {@link #getAp(AbsoluteDate date)} method returns an array of seven times the same daily Ap value,
67 * i.e. it is suited to be used only with the {@link org.orekit.models.earth.atmosphere.NRLMSISE00 NRLMSISE00} atmospheric
68 * model where the switch #9 is set to 1.
69 *
70 * <h2>References</h2>
71 *
72 * <ol> <li> Jacchia, L. G. "CIRA 1972, recent atmospheric models, and improvements in
73 * progress." COSPAR, 21st Plenary Meeting. Vol. 1. 1978. </li> </ol>
74 *
75 * @author Bruno Revelin
76 * @author Luc Maisonobe
77 * @author Evan Ward
78 * @author Pascal Parraud
79 * @author Vincent Cucchietti
80 */
81 public class MarshallSolarActivityFutureEstimation
82 extends AbstractSolarActivityData<LineParameters, MarshallSolarActivityFutureEstimationLoader> {
83
84 /**
85 * Default regular expression for the supported name that work with all officially published files.
86 *
87 * @since 10.0
88 */
89 public static final String DEFAULT_SUPPORTED_NAMES =
90 "\\p{Alpha}\\p{Lower}\\p{Lower}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}(?:f|F)10(?:[-_]prd)?\\.(?:txt|TXT)";
91
92 /** Serializable UID. */
93 private static final long serialVersionUID = -5212198874900835369L;
94
95 /** Selected strength level of activity. */
96 private final StrengthLevel strengthLevel;
97
98 /** Cache dedicated to average flux. */
99 private final transient GenericTimeStampedCache<TimeStampedDouble> averageFluxCache;
100
101 /**
102 * Simple constructor. This constructor uses the {@link DataContext#getDefault() default data context}.
103 * <p>
104 * The original file names used by NASA Marshall space center are of the form: may2019f10_prd.txt or Oct1999F10.TXT. So a
105 * recommended regular expression for the supported name that work with all published files is:
106 * {@link #DEFAULT_SUPPORTED_NAMES}.
107 * <p>
108 * It provides a default configuration for the thread safe cache :
109 * <ul>
110 * <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
111 * <li>Max span : {@code Constants.JULIAN_YEAR}</li>
112 * <li>Max span interval : {@code 0}</li>
113 * </ul>
114 *
115 * @param supportedNames regular expression for supported files names
116 * @param strengthLevel selected strength level of activity
117 *
118 * @see #MarshallSolarActivityFutureEstimation(String, StrengthLevel, DataProvidersManager, TimeScale)
119 */
120 @DefaultDataContext
121 public MarshallSolarActivityFutureEstimation(final String supportedNames,
122 final StrengthLevel strengthLevel) {
123 this(supportedNames, strengthLevel,
124 DataContext.getDefault().getDataProvidersManager(),
125 DataContext.getDefault().getTimeScales().getUTC());
126 }
127
128 /**
129 * Constructor that allows specifying the source of the MSAFE auxiliary data files.
130 * <p>
131 * It provides a default configuration for the thread safe cache :
132 * <ul>
133 * <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
134 * <li>Max span : {@code 31 * Constants.JULIAN_DAY}</li>
135 * <li>Max interval : {@code 0}</li>
136 * <li>Minimum step: {@code 27 * Constants.JULIAN_DAY}</li>
137 * </ul>
138 *
139 * @param supportedNames regular expression for supported files names
140 * @param strengthLevel selected strength level of activity
141 * @param dataProvidersManager provides access to auxiliary data files.
142 * @param utc UTC time scale.
143 *
144 * @since 10.1
145 */
146 public MarshallSolarActivityFutureEstimation(final String supportedNames,
147 final StrengthLevel strengthLevel,
148 final DataProvidersManager dataProvidersManager,
149 final TimeScale utc) {
150 this(supportedNames, strengthLevel, dataProvidersManager, utc, OrekitConfiguration.getCacheSlotsNumber(),
151 Constants.JULIAN_DAY * 31, 0, Constants.JULIAN_DAY * 27);
152 }
153
154 /**
155 * Constructor that allows specifying the source of the MSAFE auxiliary data files and customizable thread safe cache
156 * configuration.
157 *
158 * @param supportedNames regular expression for supported files names
159 * @param strengthLevel selected strength level of activity
160 * @param dataProvidersManager provides access to auxiliary data files.
161 * @param utc UTC time scale.
162 * @param maxSlots maximum number of independent cached time slots in the
163 * {@link GenericTimeStampedCache time-stamped cache}
164 * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
165 * @param maxInterval time interval above which a new slot is created in the
166 * {@link GenericTimeStampedCache time-stamped cache}
167 * @param minimumStep overriding minimum step designed for non-homogeneous tabulated values. To be used for example when
168 * caching monthly tabulated values. May be null.
169 *
170 * @since 10.1
171 */
172 public MarshallSolarActivityFutureEstimation(final String supportedNames,
173 final StrengthLevel strengthLevel,
174 final DataProvidersManager dataProvidersManager,
175 final TimeScale utc,
176 final int maxSlots,
177 final double maxSpan,
178 final double maxInterval,
179 final double minimumStep) {
180 super(supportedNames, new MarshallSolarActivityFutureEstimationLoader(strengthLevel, utc),
181 dataProvidersManager, utc, maxSlots, maxSpan, maxInterval, minimumStep);
182
183 // Initialise fields
184 this.strengthLevel = strengthLevel;
185 this.averageFluxCache = new GenericTimeStampedCache<>(N_NEIGHBORS, OrekitConfiguration.getCacheSlotsNumber(),
186 Constants.JULIAN_DAY, 0, new AverageFluxGenerator());
187 }
188
189 /**
190 * Simple constructor which use the {@link DataContext#getDefault() default data context}.
191 * <p>
192 * It provides a default configuration for the thread safe cache :
193 * <ul>
194 * <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
195 * <li>Max span : {@code 31 * Constants.JULIAN_DAY}</li>
196 * <li>Max interval : {@code 0}</li>
197 * <li>Minimum step: {@code 27 * Constants.JULIAN_DAY}</li>
198 * </ul>
199 *
200 * @param source source for the data
201 * @param strengthLevel selected strength level of activity
202 *
203 * @since 12.0
204 */
205 @DefaultDataContext
206 public MarshallSolarActivityFutureEstimation(final DataSource source,
207 final StrengthLevel strengthLevel) {
208 this(source, strengthLevel, DataContext.getDefault().getTimeScales().getUTC());
209 }
210
211 /**
212 * Simple constructor.
213 * <p>
214 * It provides a default configuration for the thread safe cache :
215 * <ul>
216 * <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
217 * <li>Max span : {@code 31 * Constants.JULIAN_DAY}</li>
218 * <li>Max interval : {@code 0}</li>
219 * <li>Minimum step: {@code 27 * Constants.JULIAN_DAY}</li>
220 * </ul>
221 *
222 * @param source source for the data
223 * @param strengthLevel selected strength level of activity
224 * @param utc UTC time scale
225 *
226 * @since 12.0
227 */
228 public MarshallSolarActivityFutureEstimation(final DataSource source,
229 final StrengthLevel strengthLevel,
230 final TimeScale utc) {
231 this(source, strengthLevel, utc, OrekitConfiguration.getCacheSlotsNumber(),
232 Constants.JULIAN_DAY * 31, 0, Constants.JULIAN_DAY * 27);
233 }
234
235 /**
236 * Constructor with customizable thread safe cache configuration.
237 *
238 * @param source source for the data
239 * @param strengthLevel selected strength level of activity
240 * @param utc UTC time scale
241 * @param maxSlots maximum number of independent cached time slots in the
242 * {@link GenericTimeStampedCache time-stamped cache}
243 * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
244 * @param maxInterval time interval above which a new slot is created in the
245 * {@link GenericTimeStampedCache time-stamped cache}
246 * @param minimumStep overriding minimum step designed for non-homogeneous tabulated values. To be used for example when
247 * caching monthly tabulated values. Use {@code Double.NaN} otherwise.
248 *
249 * @since 12.0
250 */
251 public MarshallSolarActivityFutureEstimation(final DataSource source,
252 final StrengthLevel strengthLevel,
253 final TimeScale utc,
254 final int maxSlots,
255 final double maxSpan,
256 final double maxInterval,
257 final double minimumStep) {
258 super(source, new MarshallSolarActivityFutureEstimationLoader(strengthLevel, utc), utc,
259 maxSlots, maxSpan, maxInterval, minimumStep);
260 this.strengthLevel = strengthLevel;
261 this.averageFluxCache = new GenericTimeStampedCache<>(N_NEIGHBORS, OrekitConfiguration.getCacheSlotsNumber(),
262 Constants.JULIAN_DAY, 0, new AverageFluxGenerator());
263 }
264
265 /** {@inheritDoc} */
266 public double getInstantFlux(final AbsoluteDate date) {
267 return getMeanFlux(date);
268 }
269
270 /** {@inheritDoc} */
271 public double getMeanFlux(final AbsoluteDate date) {
272 return getLinearInterpolation(date, LineParameters::getF107);
273 }
274
275 /** {@inheritDoc} */
276 public double getThreeHourlyKP(final AbsoluteDate date) {
277 return get24HoursKp(date);
278 }
279
280 /**
281 * Get the date of the file from which data at the specified date comes from.
282 * <p>
283 * If several MSAFE files are available, some dates may appear in several files (for example August 2007 is in all files
284 * from the first one published in March 1999 to the February 2008 file). In this case, the data from the most recent
285 * file is used and the older ones are discarded. The date of the file is assumed to be 6 months after its first entry
286 * (which explains why the file having August 2007 as its first entry is the February 2008 file). This implies that MSAFE
287 * files must <em>not</em> be edited to change their time span, otherwise this would break the old entries overriding
288 * mechanism.
289 * </p>
290 *
291 * @param date date of the solar activity data
292 *
293 * @return date of the file
294 */
295 public DateComponents getFileDate(final AbsoluteDate date) {
296 // Get the neighboring solar activity
297 final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
298 final LineParameters previousParam = localSolarActivity.getPreviousParam();
299 final LineParameters currentParam = localSolarActivity.getNextParam();
300
301 // Choose which file date to return
302 final double dtP = date.durationFrom(previousParam.getDate());
303 final double dtC = currentParam.getDate().durationFrom(date);
304 return (dtP < dtC) ? previousParam.getFileDate() : currentParam.getFileDate();
305 }
306
307 /**
308 * The Kp index is derived from the Ap index.
309 * <p>The method used is explained on <a
310 * href="https://www.ngdc.noaa.gov/stp/GEOMAG/kp_ap.html"> NOAA website.</a> as follows:</p>
311 * <p>The scale is 0 to 9 expressed in thirds of a unit, e.g. 5- is 4 2/3,
312 * 5 is 5 and 5+ is 5 1/3. The ap (equivalent range) index is derived from the Kp index as follows:</p>
313 * <table border="1">
314 * <caption>Kp / Ap Conversion Table</caption>
315 * <tbody>
316 * <tr>
317 * <td>Kp</td><td>0o</td><td>0+</td><td>1-</td><td>1o</td><td>1+</td><td>2-</td><td>2o</td><td>2+</td><td>3-</td><td>3o</td><td>3+</td><td>4-</td><td>4o</td><td>4+</td>
318 * </tr>
319 * <tr>
320 * <td>ap</td><td>0</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>9</td><td>12</td><td>15</td><td>18</td><td>22</td><td>27</td><td>32</td>
321 * </tr>
322 * <tr>
323 * <td>Kp</td><td>5-</td><td>5o</td><td>5+</td><td>6-</td><td>6o</td><td>6+</td><td>7-</td><td>7o</td><td>7+</td><td>8-</td><td>8o</td><td>8+</td><td>9-</td><td>9o</td>
324 * </tr>
325 * <tr>
326 * <td>ap</td><td>39</td><td>48</td><td>56</td><td>67</td><td>80</td><td>94</td><td>111</td><td>132</td><td>154</td><td>179</td><td>207</td><td>236</td><td>300</td><td>400</td>
327 * </tr>
328 * </tbody>
329 * </table>
330 *
331 * @param date date of the Kp data
332 *
333 * @return the 24H geomagnetic index
334 */
335 public double get24HoursKp(final AbsoluteDate date) {
336 // get the daily Ap
337 final double ap = getDailyAp(date);
338
339 // get the corresponding Kp index from
340 // equation 4 in [1] for Ap to Kp conversion
341 return 1.89 * FastMath.asinh(0.154 * ap);
342 }
343
344 /** {@inheritDoc} */
345 public double getDailyFlux(final AbsoluteDate date) {
346 return getMeanFlux(date.shiftedBy(-Constants.JULIAN_DAY));
347 }
348
349 public double getAverageFlux(final AbsoluteDate date) {
350 // Extract closest neighbours average
351 final List<TimeStampedDouble> neighbors = averageFluxCache.getNeighbors(date).collect(Collectors.toList());
352
353 // Create linear interpolating function
354 final double[] x = new double[] { 0, 1 };
355 final double[] y = neighbors.stream().map(TimeStampedDouble::getValue).mapToDouble(Double::doubleValue).toArray();
356
357 final LinearInterpolator interpolator = new LinearInterpolator();
358 final UnivariateFunction interpolatingFunction = interpolator.interpolate(x, y);
359
360 // Interpolate
361 final AbsoluteDate previousDate = neighbors.get(0).getDate();
362 final AbsoluteDate nextDate = neighbors.get(1).getDate();
363
364 // TODO Temporary fix for issue 1719 until GenericTimeStampedCache is fixed
365 final double normInterpDate = date.durationFrom(previousDate) / nextDate.durationFrom(previousDate);
366 return interpolatingFunction.value(FastMath.max(0, FastMath.min(normInterpDate, 1.0)));
367 }
368
369 /** {@inheritDoc} */
370 public double[] getAp(final AbsoluteDate date) {
371 // Gets the AP for the current date
372 final double ap = getDailyAp(date);
373
374 // Retuns an array of Ap filled in with the daily Ap only
375 return new double[] { ap, ap, ap, ap, ap, ap, ap };
376 }
377
378 /**
379 * Gets the daily Ap index.
380 *
381 * @param date the current date
382 *
383 * @return the daily Ap index
384 */
385 private double getDailyAp(final AbsoluteDate date) {
386 return getLinearInterpolation(date, LineParameters::getAp);
387 }
388
389 /**
390 * Get the strength level for activity.
391 *
392 * @return strength level to set
393 */
394 public StrengthLevel getStrengthLevel() {
395 return strengthLevel;
396 }
397
398 /** Strength level of activity. */
399 public enum StrengthLevel {
400
401 /** Strong level of activity. */
402 STRONG,
403
404 /** Average level of activity. */
405 AVERAGE,
406
407 /** Weak level of activity. */
408 WEAK
409
410 }
411
412 /** Generator generating average flux data between given dates. */
413 private class AverageFluxGenerator implements TimeStampedGenerator<TimeStampedDouble> {
414
415 /** {@inheritDoc} */
416 @Override
417 public List<TimeStampedDouble> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {
418 // No prior data in the cache
419 if (existingDate == null) {
420 return generateDataFromEarliestToLatestDates(getCurrentDay(date), getNextDay(date));
421 }
422 // Prior data in the cache, fill with data between date and existing date
423 if (date.isBefore(existingDate)) {
424 return generateDataFromEarliestToLatestDates(date, existingDate);
425 }
426 return generateDataFromEarliestToLatestDates(existingDate, date);
427 }
428
429 /**
430 * Generate data from earliest to latest date.
431 *
432 * @param earliest earliest date
433 * @param latest latest date
434 *
435 * @return data generated from earliest to latest date
436 */
437 private List<TimeStampedDouble> generateDataFromEarliestToLatestDates(final AbsoluteDate earliest,
438 final AbsoluteDate latest) {
439 final List<TimeStampedDouble> generated = new ArrayList<>();
440
441 // Add next computed average until it brackets the latest date
442 AbsoluteDate latestNeighbourDate = getCurrentDay(earliest);
443 while (latestNeighbourDate.isBeforeOrEqualTo(latest)) {
444 generated.add(computeAverageFlux(latestNeighbourDate));
445 latestNeighbourDate = getNextDay(latestNeighbourDate);
446 }
447 return generated;
448 }
449
450 /**
451 * Get the current day at midnight.
452 *
453 * @param date date
454 *
455 * @return current day at midnight.
456 */
457 private AbsoluteDate getCurrentDay(final AbsoluteDate date) {
458 // Find previous day date time components
459 final TimeScale utc = getUTC();
460 final DateComponents dateComponents = date.getComponents(utc).getDate();
461
462 // Create absolute date for previous day
463 return new AbsoluteDate(new DateTimeComponents(dateComponents, TimeComponents.H00), utc);
464 }
465
466 /**
467 * Get the next day at midnight.
468 *
469 * @param date date
470 *
471 * @return next day at midnight.
472 */
473 private AbsoluteDate getNextDay(final AbsoluteDate date) {
474 // Find previous day date time components
475 final TimeScale utc = getUTC();
476 final DateComponents dateComponents = date.getComponents(utc).getDate();
477 final DateComponents shiftedComponents = new DateComponents(dateComponents, 1);
478
479 // Create absolute date for previous day
480 return new AbsoluteDate(new DateTimeComponents(shiftedComponents, TimeComponents.H00), utc);
481 }
482
483 /**
484 * Compute the average flux for given absolute date.
485 *
486 * @param date date at which the average flux is desired
487 *
488 * @return average flux
489 */
490 private TimeStampedDouble computeAverageFlux(final AbsoluteDate date) {
491 // Extract list of neighbors to compute average
492 final TimeStampedGenerator<LineParameters> generator = getCache().getGenerator();
493 final AbsoluteDate initialDate = date.shiftedBy(-40 * Constants.JULIAN_DAY);
494 final AbsoluteDate finalDate = date.shiftedBy(40 * Constants.JULIAN_DAY);
495 final List<LineParameters> monthlyData = generator.generate(initialDate, finalDate);
496
497 // Create interpolator for given data
498 final LinearInterpolator interpolator = new LinearInterpolator();
499
500 final double[] x = monthlyData.stream().map(param -> param.getDate().durationFrom(initialDate))
501 .mapToDouble(Double::doubleValue).toArray();
502 final double[] y = monthlyData.stream().map(LineParameters::getF107).mapToDouble(Double::doubleValue).toArray();
503
504 final UnivariateFunction interpolatingFunction = interpolator.interpolate(x, y);
505
506 // Loops over the 81 days centered on the given date
507 double average = 0;
508 for (int i = -40; i < 41; i++) {
509 average += interpolatingFunction.value(date.shiftedBy(i * Constants.JULIAN_DAY).durationFrom(initialDate));
510 }
511
512 // Returns the 81 day average flux
513 return new TimeStampedDouble(date, average / 81);
514 }
515 }
516
517 }