MarshallSolarActivityFutureEstimationLoader.java

  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. import org.orekit.annotation.DefaultDataContext;
  19. import org.orekit.data.DataContext;
  20. import org.orekit.errors.OrekitException;
  21. import org.orekit.errors.OrekitMessages;
  22. import org.orekit.models.earth.atmosphere.data.MarshallSolarActivityFutureEstimation.StrengthLevel;
  23. import org.orekit.time.AbsoluteDate;
  24. import org.orekit.time.ChronologicalComparator;
  25. import org.orekit.time.DateComponents;
  26. import org.orekit.time.Month;
  27. import org.orekit.time.TimeScale;
  28. import org.orekit.time.TimeStamped;

  29. import java.io.BufferedReader;
  30. import java.io.IOException;
  31. import java.io.InputStream;
  32. import java.io.InputStreamReader;
  33. import java.nio.charset.StandardCharsets;
  34. import java.text.ParseException;
  35. import java.util.Iterator;
  36. import java.util.SortedSet;
  37. import java.util.TreeSet;
  38. import java.util.regex.Matcher;
  39. import java.util.regex.Pattern;
  40. import java.util.stream.Collectors;

  41. /**
  42.  * This class reads solar activity data needed by atmospheric models: F107 solar flux, Ap and Kp indexes.
  43.  * <p>
  44.  * The data are retrieved through the NASA Marshall Solar Activity Future Estimation (MSAFE) as estimates of monthly F10.7
  45.  * Mean solar flux and Ap geomagnetic parameter. The data can be retrieved at the NASA <a
  46.  * href="https://www.nasa.gov/msfcsolar/archivedforecast"> Marshall Solar Activity website</a>. Here Kp indices are deduced
  47.  * from Ap indexes, which in turn are tabulated equivalent of retrieved Ap values.
  48.  * </p>
  49.  * <p>
  50.  * If several MSAFE files are available, some dates may appear in several files (for example August 2007 is in all files from
  51.  * the first one published in March 1999 to the February 2008 file). In this case, the data from the most recent file is used
  52.  * and the older ones are discarded. The date of the file is assumed to be 6 months after its first entry (which explains why
  53.  * the file having August 2007 as its first entry is the February 2008 file). This implies that MSAFE files must <em>not</em>
  54.  * be edited to change their time span, otherwise this would break the old entries overriding mechanism.
  55.  * </p>
  56.  *
  57.  * <h2>References</h2>
  58.  *
  59.  * <ol> <li> Jacchia, L. G. "CIRA 1972, recent atmospheric models, and improvements in
  60.  * progress." COSPAR, 21st Plenary Meeting. Vol. 1. 1978. </li> </ol>
  61.  *
  62.  * @author Bruno Revelin
  63.  * @author Luc Maisonobe
  64.  * @author Evan Ward
  65.  * @author Pascal Parraud
  66.  * @author Vincent Cucchietti
  67.  */
  68. public class MarshallSolarActivityFutureEstimationLoader
  69.         extends AbstractSolarActivityDataLoader<MarshallSolarActivityFutureEstimationLoader.LineParameters> {

  70.     /** Pattern for the data fields of MSAFE data. */
  71.     private final Pattern dataPattern;

  72.     /** Data set. */
  73.     private final SortedSet<TimeStamped> data;

  74.     /** Selected strength level of activity. */
  75.     private final StrengthLevel strengthLevel;

  76.     /**
  77.      * Simple constructor. This constructor uses the {@link DataContext#getDefault() default data context}.
  78.      *
  79.      * @param strengthLevel selected strength level of activity
  80.      */
  81.     @DefaultDataContext
  82.     public MarshallSolarActivityFutureEstimationLoader(final StrengthLevel strengthLevel) {
  83.         this(strengthLevel, DataContext.getDefault().getTimeScales().getUTC());
  84.     }

  85.     /**
  86.      * Constructor that allows specifying the source of the MSAFE auxiliary data files.
  87.      *
  88.      * @param strengthLevel selected strength level of activity
  89.      * @param utc UTC time scale.
  90.      *
  91.      * @since 10.1
  92.      */
  93.     public MarshallSolarActivityFutureEstimationLoader(final StrengthLevel strengthLevel, final TimeScale utc) {
  94.         super(utc);

  95.         this.data          = new TreeSet<>(new ChronologicalComparator());
  96.         this.strengthLevel = strengthLevel;

  97.         // the data lines have the following form:
  98.         // 2010.5003   JUL    83.4      81.3      78.7       6.4       5.9       5.2
  99.         // 2010.5837   AUG    87.3      83.4      78.5       7.0       6.1       4.9
  100.         // 2010.6670   SEP    90.8      85.5      79.4       7.8       6.2       4.7
  101.         // 2010.7503   OCT    94.2      87.6      80.4       9.1       6.4       4.9
  102.         final StringBuilder builder = new StringBuilder("^");

  103.         // first group: year
  104.         builder.append("\\p{Blank}*(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})");

  105.         // month as fraction of year, not stored in a group
  106.         builder.append("\\.\\p{Digit}+");

  107.         // second group: month as a three upper case letters abbreviation
  108.         builder.append("\\p{Blank}+(");
  109.         for (final Month month : Month.values()) {
  110.             builder.append(month.getUpperCaseAbbreviation());
  111.             builder.append('|');
  112.         }
  113.         builder.delete(builder.length() - 1, builder.length());
  114.         builder.append(")");

  115.         // third to eighth group: data fields
  116.         for (int i = 0; i < 6; ++i) {
  117.             builder.append("\\p{Blank}+([-+]?[0-9]+\\.[0-9]+)");
  118.         }

  119.         // end of line
  120.         builder.append("\\p{Blank}*$");

  121.         // compile the pattern
  122.         this.dataPattern = Pattern.compile(builder.toString());

  123.     }

  124.     /** {@inheritDoc} */
  125.     public void loadData(final InputStream input, final String name)
  126.             throws IOException, ParseException, OrekitException {

  127.         // select the groups we want to store
  128.         final int f107Group;
  129.         final int apGroup;
  130.         switch (strengthLevel) {
  131.             case STRONG:
  132.                 f107Group = 3;
  133.                 apGroup = 6;
  134.                 break;
  135.             case AVERAGE:
  136.                 f107Group = 4;
  137.                 apGroup = 7;
  138.                 break;
  139.             default:
  140.                 f107Group = 5;
  141.                 apGroup = 8;
  142.                 break;
  143.         }

  144.         boolean        inData   = false;
  145.         DateComponents fileDate = null;

  146.         // try to read the data
  147.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {

  148.             // Go through each line
  149.             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  150.                 line = line.trim();
  151.                 if (!line.isEmpty()) {
  152.                     final Matcher matcher = dataPattern.matcher(line);
  153.                     if (matcher.matches()) {

  154.                         // We are in the data section
  155.                         inData = true;

  156.                         // Extract the data from the line
  157.                         final int          year  = Integer.parseInt(matcher.group(1));
  158.                         final Month        month = Month.parseMonth(matcher.group(2));
  159.                         final AbsoluteDate date  = new AbsoluteDate(year, month, 1, getUTC());
  160.                         if (fileDate == null) {
  161.                             /* The first entry of each file correspond exactly to 6 months before file publication,
  162.                             so we compute the file date by adding 6 months to its first entry */
  163.                             if (month.getNumber() > 6) {
  164.                                 fileDate = new DateComponents(year + 1, month.getNumber() - 6, 1);
  165.                             } else {
  166.                                 fileDate = new DateComponents(year, month.getNumber() + 6, 1);
  167.                             }
  168.                         }

  169.                         // check if there is already an entry for this date or not
  170.                         boolean                     addEntry = false;
  171.                         final Iterator<TimeStamped> iterator = data.tailSet(date).iterator();
  172.                         if (iterator.hasNext()) {
  173.                             final LineParameters existingEntry = (LineParameters) iterator.next();
  174.                             if (existingEntry.getDate().equals(date)) {
  175.                                 // there is an entry for this date
  176.                                 if (existingEntry.getFileDate().compareTo(fileDate) < 0) {
  177.                                     // the entry was read from an earlier file
  178.                                     // we replace it with the new entry as it is fresher
  179.                                     iterator.remove();
  180.                                     addEntry = true;
  181.                                 }
  182.                             } else {
  183.                                 // it is the first entry we get for this date
  184.                                 addEntry = true;
  185.                             }
  186.                         } else {
  187.                             // it is the first entry we get for this date
  188.                             addEntry = true;
  189.                         }
  190.                         if (addEntry) {
  191.                             // we must add the new entry
  192.                             data.add(new LineParameters(fileDate, date,
  193.                                                         Double.parseDouble(matcher.group(f107Group)),
  194.                                                         Double.parseDouble(matcher.group(apGroup))));
  195.                         }

  196.                     } else {
  197.                         if (inData) {
  198.                             /* We have already read some data, so we are not in the header anymore
  199.                             however, we don't recognize this non-empty line, we consider the file is corrupted */
  200.                             throw new OrekitException(OrekitMessages.NOT_A_MARSHALL_SOLAR_ACTIVITY_FUTURE_ESTIMATION_FILE,
  201.                                                       name);
  202.                         }
  203.                     }
  204.                 }
  205.             }

  206.         }

  207.         if (data.isEmpty()) {
  208.             throw new OrekitException(OrekitMessages.NOT_A_MARSHALL_SOLAR_ACTIVITY_FUTURE_ESTIMATION_FILE, name);
  209.         }
  210.         setMinDate(data.first().getDate());
  211.         setMaxDate(data.last().getDate());

  212.     }

  213.     /** @return the data set */
  214.     @Override
  215.     public SortedSet<LineParameters> getDataSet() {
  216.         return data.stream().map(value -> (LineParameters) value).collect(Collectors.toCollection(TreeSet::new));
  217.     }

  218.     /** Container class for Solar activity indexes. */
  219.     public static class LineParameters extends AbstractSolarActivityDataLoader.LineParameters {

  220.         /** Serializable UID. */
  221.         private static final long serialVersionUID = 6607862001953526475L;

  222.         /** File date. */
  223.         private final DateComponents fileDate;

  224.         /** F10.7 flux at date. */
  225.         private final double f107;

  226.         /** Ap index at date. */
  227.         private final double ap;

  228.         /**
  229.          * Simple constructor.
  230.          *
  231.          * @param fileDate file date
  232.          * @param date entry date
  233.          * @param f107 F10.7 flux at date
  234.          * @param ap Ap index at date
  235.          */
  236.         private LineParameters(final DateComponents fileDate, final AbsoluteDate date, final double f107, final double ap) {
  237.             super(date);
  238.             this.fileDate = fileDate;
  239.             this.f107     = f107;
  240.             this.ap       = ap;
  241.         }

  242.         /** {@inheritDoc} */
  243.         @Override
  244.         public int compareTo(final AbstractSolarActivityDataLoader.LineParameters lineParameters) {
  245.             return getDate().compareTo(lineParameters.getDate());
  246.         }

  247.         /** {@inheritDoc} */
  248.         @Override
  249.         public boolean equals(final Object otherInstance) {
  250.             if (this == otherInstance) {
  251.                 return true;
  252.             }
  253.             if (otherInstance == null || getClass() != otherInstance.getClass()) {
  254.                 return false;
  255.             }

  256.             final LineParameters msafeParams = (LineParameters) otherInstance;

  257.             if (Double.compare(getF107(), msafeParams.getF107()) != 0) {
  258.                 return false;
  259.             }
  260.             if (Double.compare(getAp(), msafeParams.getAp()) != 0) {
  261.                 return false;
  262.             }
  263.             return getFileDate().equals(msafeParams.getFileDate());
  264.         }

  265.         /** {@inheritDoc} */
  266.         @Override
  267.         public int hashCode() {
  268.             int  result;
  269.             result = getFileDate().hashCode();
  270.             result = 31 * result + Double.hashCode(getF107());
  271.             result = 31 * result + Double.hashCode(getAp());
  272.             return result;
  273.         }

  274.         /**
  275.          * Get the file date.
  276.          *
  277.          * @return file date
  278.          */
  279.         public DateComponents getFileDate() {
  280.             return fileDate;
  281.         }

  282.         /**
  283.          * Get the F10.0 flux.
  284.          *
  285.          * @return f10.7 flux
  286.          */
  287.         public double getF107() {
  288.             return f107;
  289.         }

  290.         /**
  291.          * Get the Ap index.
  292.          *
  293.          * @return Ap index
  294.          */
  295.         public double getAp() {
  296.             return ap;
  297.         }

  298.     }

  299. }