1   /* Copyright 2020-2025 Clément Jonglez
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    * Clément Jonglez 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  
18  package org.orekit.models.earth.atmosphere.data;
19  
20  import org.orekit.annotation.DefaultDataContext;
21  import org.orekit.data.DataContext;
22  import org.orekit.data.DataProvidersManager;
23  import org.orekit.data.DataSource;
24  import org.orekit.models.earth.atmosphere.data.CssiSpaceWeatherDataLoader.LineParameters;
25  import org.orekit.time.AbsoluteDate;
26  import org.orekit.time.TimeScale;
27  import org.orekit.utils.Constants;
28  import org.orekit.utils.GenericTimeStampedCache;
29  import org.orekit.utils.OrekitConfiguration;
30  
31  /**
32   * This class provides three-hourly and daily solar activity data needed by atmospheric models: F107 solar flux, Ap and Kp
33   * indexes. The {@link org.orekit.data.DataLoader} implementation and the parsing is handled by the class
34   * {@link CssiSpaceWeatherDataLoader}.
35   * <p>
36   * The data are retrieved through space weather files offered by AGI/CSSI on the AGI
37   * <a href="https://ftp.agi.com/pub/DynamicEarthData/SpaceWeather-All-v1.2.txt">FTP</a> as
38   * well as on the CelesTrack <a href="http://celestrak.com/SpaceData/">website</a>. These files are updated several times a
39   * day by using several sources mentioned in the
40   * <a href="http://celestrak.com/SpaceData/SpaceWx-format.php">Celestrak space
41   * weather data documentation</a>.
42   * </p>
43   *
44   * @author Clément Jonglez
45   * @author Vincent Cucchietti
46   * @since 10.2
47   */
48  public class CssiSpaceWeatherData extends AbstractSolarActivityData<LineParameters, CssiSpaceWeatherDataLoader> {
49  
50      /** Default regular expression for supported names that works with all officially published files. */
51      public static final String DEFAULT_SUPPORTED_NAMES = "^S(?:pace)?W(?:eather)?-(?:All)?.*\\.txt$";
52  
53      /** Serializable UID. */
54      private static final long serialVersionUID = 4249411710645968978L;
55  
56      /** Date of last data before the prediction starts. */
57      private final AbsoluteDate lastObservedDate;
58  
59      /** Date of last daily prediction before the monthly prediction starts. */
60      private final AbsoluteDate lastDailyPredictedDate;
61  
62      /**
63       * Simple constructor. This constructor uses the default data context.
64       * <p>
65       * The original file names provided by AGI/CSSI are of the form: SpaceWeather-All-v1.2.txt
66       * (<a href="https://ftp.agi.com/pub/DynamicEarthData/SpaceWeather-All-v1.2.txt">AGI's ftp</a>). So a recommended regular
67       * expression for the supported names that works with all published files is: {@link #DEFAULT_SUPPORTED_NAMES}.
68       * <p>
69       * It provides a default configuration for the thread safe cache :
70       * <ul>
71       *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
72       *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
73       *     <li>Max span interval : {@code 0}</li>
74       * </ul>
75       *
76       * @param supportedNames regular expression for supported AGI/CSSI space weather files names
77       */
78      @DefaultDataContext
79      public CssiSpaceWeatherData(final String supportedNames) {
80          this(supportedNames, DataContext.getDefault().getDataProvidersManager(),
81               DataContext.getDefault().getTimeScales().getUTC());
82      }
83  
84      /**
85       * Constructor that allows specifying the source of the CSSI space weather file.
86       * <p>
87       * It provides a default configuration for the thread safe cache :
88       * <ul>
89       *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
90       *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
91       *     <li>Max span interval : {@code 0}</li>
92       * </ul>
93       *
94       * @param supportedNames regular expression for supported AGI/CSSI space weather files names
95       * @param dataProvidersManager provides access to auxiliary data files.
96       * @param utc UTC time scale.
97       */
98      public CssiSpaceWeatherData(final String supportedNames, final DataProvidersManager dataProvidersManager,
99                                  final TimeScale utc) {
100         this(supportedNames, new CssiSpaceWeatherDataLoader(utc), dataProvidersManager, utc);
101     }
102 
103     /**
104      * Constructor that allows specifying the source of the CSSI space weather file.
105      * <p>
106      * It provides a default configuration for the thread safe cache :
107      * <ul>
108      *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
109      *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
110      *     <li>Max span interval : {@code 0}</li>
111      * </ul>
112      *
113      * @param supportedNames regular expression for supported AGI/CSSI space weather files names
114      * @param loader data loader
115      * @param dataProvidersManager provides access to auxiliary data files.
116      * @param utc UTC time scale
117      */
118     public CssiSpaceWeatherData(final String supportedNames, final CssiSpaceWeatherDataLoader loader,
119                                 final DataProvidersManager dataProvidersManager, final TimeScale utc) {
120         this(supportedNames, loader, dataProvidersManager, utc, OrekitConfiguration.getCacheSlotsNumber(),
121              Constants.JULIAN_DAY, 0);
122     }
123 
124     /**
125      * Constructor that allows specifying the source of the CSSI space weather file and customizable thread safe cache
126      * configuration.
127      *
128      * @param supportedNames regular expression for supported AGI/CSSI space weather files names
129      * @param loader data loader
130      * @param dataProvidersManager provides access to auxiliary data files.
131      * @param utc UTC time scale
132      * @param maxSlots maximum number of independent cached time slots in the
133      * {@link GenericTimeStampedCache time-stamped cache}
134      * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
135      * @param maxInterval time interval above which a new slot is created in the
136      * {@link GenericTimeStampedCache time-stamped cache}
137      */
138     public CssiSpaceWeatherData(final String supportedNames, final CssiSpaceWeatherDataLoader loader,
139                                 final DataProvidersManager dataProvidersManager, final TimeScale utc, final int maxSlots,
140                                 final double maxSpan, final double maxInterval) {
141         super(supportedNames, loader, dataProvidersManager, utc, maxSlots, maxSpan, maxInterval, Constants.JULIAN_DAY);
142 
143         // Initialise fields
144         this.lastObservedDate       = loader.getLastObservedDate();
145         this.lastDailyPredictedDate = loader.getLastDailyPredictedDate();
146     }
147 
148     /**
149      * Simple constructor which use the {@link DataContext#getDefault() default data context}.
150      * <p>
151      * It provides a default configuration for the thread safe cache :
152      * <ul>
153      *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
154      *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
155      *     <li>Max span interval : {@code 0}</li>
156      * </ul>
157      *
158      * @param source source for the data
159      *
160      * @since 12.0
161      */
162     @DefaultDataContext
163     public CssiSpaceWeatherData(final DataSource source) {
164         this(source, DataContext.getDefault().getTimeScales().getUTC());
165     }
166 
167     /**
168      * Simple constructor.
169      * <p>
170      * It provides a default configuration for the thread safe cache :
171      * <ul>
172      *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
173      *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
174      *     <li>Max span interval : {@code 0}</li>
175      * </ul>
176      *
177      * @param source source for the data
178      * @param utc UTC time scale
179      *
180      * @since 12.0
181      */
182     public CssiSpaceWeatherData(final DataSource source, final TimeScale utc) {
183         this(source, new CssiSpaceWeatherDataLoader(utc), utc);
184     }
185 
186     /**
187      * Simple constructor.
188      * <p>
189      * It provides a default configuration for the thread safe cache :
190      * <ul>
191      *     <li>Number of slots : {@code OrekitConfiguration.getCacheSlotsNumber()}</li>
192      *     <li>Max span : {@code Constants.JULIAN_DAY}</li>
193      *     <li>Max span interval : {@code 0}</li>
194      * </ul>
195      *
196      * @param source source for the data
197      * @param loader data loader
198      * @param utc UTC time scale
199      *
200      * @since 12.0
201      */
202     public CssiSpaceWeatherData(final DataSource source, final CssiSpaceWeatherDataLoader loader, final TimeScale utc) {
203         this(source, loader, utc, OrekitConfiguration.getCacheSlotsNumber(), Constants.JULIAN_DAY, 0);
204     }
205 
206     /**
207      * Simple constructor with customizable thread safe cache configuration.
208      *
209      * @param source source for the data
210      * @param loader data loader
211      * @param utc UTC time scale
212      * @param maxSlots maximum number of independent cached time slots in the
213      * {@link GenericTimeStampedCache time-stamped cache}
214      * @param maxSpan maximum duration span in seconds of one slot in the {@link GenericTimeStampedCache time-stamped cache}
215      * @param maxInterval time interval above which a new slot is created in the
216      * {@link GenericTimeStampedCache time-stamped cache}
217      *
218      * @since 12.0
219      */
220     public CssiSpaceWeatherData(final DataSource source, final CssiSpaceWeatherDataLoader loader, final TimeScale utc,
221                                 final int maxSlots, final double maxSpan, final double maxInterval) {
222         super(source, loader, utc, maxSlots, maxSpan, maxInterval, Constants.JULIAN_DAY);
223         this.lastObservedDate       = loader.getLastObservedDate();
224         this.lastDailyPredictedDate = loader.getLastDailyPredictedDate();
225     }
226 
227     /** {@inheritDoc} */
228     public double getInstantFlux(final AbsoluteDate date) {
229         return getLinearInterpolation(date, LineParameters::getF107Obs);
230     }
231 
232     /** {@inheritDoc} */
233     public double getMeanFlux(final AbsoluteDate date) {
234         return getAverageFlux(date);
235     }
236 
237     /** {@inheritDoc} */
238     public double getThreeHourlyKP(final AbsoluteDate date) {
239         if (date.compareTo(lastObservedDate) <= 0) {
240             /* If observation data is available, it contains three-hourly data */
241             final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
242             final LineParameters     previousParam      = localSolarActivity.getPreviousParam();
243             final double             hourOfDay          = date.offsetFrom(previousParam.getDate(), getUTC()) / 3600;
244             int                      i_kp               = (int) (hourOfDay / 3);
245             if (i_kp >= 8) {
246                 /* hourOfDay can take the value 24.0 at midnight due to floating point precision
247                  * when bracketing the dates or during a leap second because the hour of day is
248                  * computed in UTC view */
249                 i_kp = 7;
250             }
251             return previousParam.getThreeHourlyKp(i_kp);
252         } else {
253             /* Only predictions are available, there are no three-hourly data */
254             return get24HoursKp(date);
255         }
256     }
257 
258     /** {@inheritDoc} */
259     public double get24HoursKp(final AbsoluteDate date) {
260         // Get the neighboring solar activity
261         final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
262 
263         if (date.compareTo(lastDailyPredictedDate) <= 0) {
264             // Daily data is available, just taking the daily average
265             return localSolarActivity.getPreviousParam().getKpSum() / 8;
266         } else {
267             // Only monthly data is available, better interpolate between two months
268             return getLinearInterpolation(localSolarActivity, lineParam -> lineParam.getKpSum() / 8);
269         }
270     }
271 
272     /** {@inheritDoc} */
273     public double getDailyFlux(final AbsoluteDate date) {
274         // Getting the value for the previous day
275         return getDailyFluxOnDay(date.shiftedBy(-Constants.JULIAN_DAY));
276     }
277 
278     /** {@inheritDoc} */
279     public double getAverageFlux(final AbsoluteDate date) {
280         // Get the neighboring solar activity
281         final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
282 
283         if (date.compareTo(lastDailyPredictedDate) <= 0) {
284             return localSolarActivity.getPreviousParam().getCtr81Obs();
285         } else {
286             // Only monthly data is available, better interpolate between two months
287             return getLinearInterpolation(localSolarActivity, LineParameters::getCtr81Obs);
288         }
289     }
290 
291     /** {@inheritDoc} */
292     public double[] getAp(final AbsoluteDate date) {
293         final double[] apArray = new double[7];
294         apArray[0] = getDailyAp(date);
295         apArray[1] = getThreeHourlyAp(date);
296         apArray[2] = getThreeHourlyAp(date.shiftedBy(-3.0 * 3600.0));
297         apArray[3] = getThreeHourlyAp(date.shiftedBy(-6.0 * 3600.0));
298         apArray[4] = getThreeHourlyAp(date.shiftedBy(-9.0 * 3600.0));
299         apArray[5] = get24HoursAverageAp(date.shiftedBy(-12.0 * 3600.0));
300         apArray[6] = get24HoursAverageAp(date.shiftedBy(-36.0 * 3600.0));
301         return apArray;
302     }
303 
304     /**
305      * Gets the daily flux on the current day.
306      *
307      * @param date the current date
308      *
309      * @return the daily F10.7 flux (observed)
310      */
311     private double getDailyFluxOnDay(final AbsoluteDate date) {
312         // Get the neighboring solar activity
313         final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
314 
315         if (date.compareTo(lastDailyPredictedDate) <= 0) {
316             // Getting the value for the previous day
317             return localSolarActivity.getPreviousParam().getF107Obs();
318         } else {
319             // Only monthly data is available, better interpolate between two months
320             return getLinearInterpolation(localSolarActivity, LineParameters::getF107Obs);
321         }
322     }
323 
324     /**
325      * Gets the value of the three-hourly Ap index for the given date.
326      *
327      * @param date the current date
328      *
329      * @return the current three-hourly Ap index
330      */
331     private double getThreeHourlyAp(final AbsoluteDate date) {
332         if (date.compareTo(lastObservedDate.shiftedBy(Constants.JULIAN_DAY)) < 0) {
333             // If observation data is available, it contains three-hourly data.
334             // Get the neighboring solar activity
335             final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
336 
337             final LineParameters previousParam = localSolarActivity.getPreviousParam();
338             final double         hourOfDay     = date.offsetFrom(previousParam.getDate(), getUTC()) / 3600;
339             int                  i_ap          = (int) (hourOfDay / 3);
340             if (i_ap >= 8) {
341                 /* hourOfDay can take the value 24.0 at midnight due to floating point precision
342                  * when bracketing the dates or during a leap second because the hour of day is
343                  * computed in UTC view */
344                 i_ap = 7;
345             }
346             return previousParam.getThreeHourlyAp(i_ap);
347         } else {
348             /* Only predictions are available, there are no three-hourly data */
349             return getDailyAp(date);
350         }
351     }
352 
353     /**
354      * Gets the running average of the 8 three-hourly Ap indices prior to current time If three-hourly data is available, the
355      * result is different than getDailyAp.
356      *
357      * @param date the current date
358      *
359      * @return the 24 hours running average of the Ap index
360      */
361     private double get24HoursAverageAp(final AbsoluteDate date) {
362         if (date.compareTo(lastDailyPredictedDate) <= 0) {
363             // Computing running mean
364             double apSum = 0.0;
365             for (int i = 0; i < 8; i++) {
366                 apSum += getThreeHourlyAp(date.shiftedBy(-3.0 * 3600 * i));
367             }
368             return apSum / 8;
369         } else {
370             /* Only monthly predictions are available, no need to compute the average from
371              * three hourly data */
372             return getDailyAp(date);
373         }
374     }
375 
376     /**
377      * Get the daily Ap index for the given date.
378      *
379      * @param date the current date
380      *
381      * @return the daily Ap index
382      */
383     private double getDailyAp(final AbsoluteDate date) {
384         // Get the neighboring solar activity
385         final LocalSolarActivity localSolarActivity = new LocalSolarActivity(date);
386 
387         if (date.compareTo(lastDailyPredictedDate) <= 0) {
388             // Daily data is available, just taking the daily average
389             return localSolarActivity.getPreviousParam().getApAvg();
390         } else {
391             // Only monthly data is available, better interpolate between two months
392             return getLinearInterpolation(localSolarActivity, LineParameters::getApAvg);
393         }
394     }
395 
396 }