ModipGrid.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.ionosphere.nequick;

  18. import org.hipparchus.CalculusFieldElement;
  19. import org.hipparchus.util.FastMath;
  20. import org.hipparchus.util.MathUtils;
  21. import org.orekit.data.DataSource;
  22. import org.orekit.errors.OrekitException;
  23. import org.orekit.errors.OrekitMessages;

  24. import java.io.BufferedReader;
  25. import java.io.IOException;
  26. import java.io.Reader;
  27. import java.util.regex.Pattern;

  28. /** Modified Dip Latitude grid.
  29.  * <p>
  30.  * The modip grid allows to estimate modip μ [deg] at a given point (φ,λ)
  31.  * by interpolation of the relevant values contained in the support file.
  32.  * </p>
  33.  * <p>
  34.  * The data is parsed from files sampling Earth in latitude and longitude. In
  35.  * the case of Galileo-specific modip file, sampling step is 5° in latitude and
  36.  * 10° in longitude, and extra rows/columns have been added to wrap around
  37.  * anti-meridian and longitude and also "past" pole (adding extra points with
  38.  * 180° longitude phasing offset) in latitude. In the case of ITU original
  39.  * NeQuick 2 model, sampling step is 1° in latitude and 2° in longitude, without
  40.  * any extra rows/columns. We therefore add the extra rows/columns upon parsing
  41.  * the ITU file.
  42.  * </p>
  43.  * @author Bryan Cazabonne
  44.  * @author Luc Maisonobe
  45.  * @since 13.0
  46.  */
  47. class ModipGrid {

  48.     /** Pattern for delimiting regular expressions. */
  49.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  50.     /** Number of cells of modip grid in longitude (without wrapping). */
  51.     private final int nbCellsLon;

  52.     /** Cell size in longitude. */
  53.     private final double sizeLon;

  54.     /** Number of cells of modip grid in latitude (without wrapping). */
  55.     private final int nbCellsLat;

  56.     /** Cell size in latitude. */
  57.     private final double sizeLat;

  58.     /** Modip grid. */
  59.     private final double[][] grid;

  60.     /** Build a new modip grid.
  61.      * @param nbCellsLon number of cells of modip grid in longitude (without wrapping)
  62.      * @param nbCellsLat number of cells of modip grid in latitude (without wrapping)
  63.      * @param source source of the grid file
  64.      * @param wrappingAlreadyIncluded indicator for already included modip grid wrapping in resource file
  65.      */
  66.     ModipGrid(final int nbCellsLon, final int nbCellsLat, final DataSource source,
  67.               final boolean wrappingAlreadyIncluded) {
  68.         this.nbCellsLon = nbCellsLon;
  69.         this.sizeLon    = MathUtils.TWO_PI / nbCellsLon;
  70.         this.nbCellsLat = nbCellsLat;
  71.         this.sizeLat    = FastMath.PI / nbCellsLat;
  72.         this.grid       = new double[nbCellsLat + 3][nbCellsLon + 3];
  73.         loadData(source, !wrappingAlreadyIncluded);
  74.     }

  75.     /** Compute modip for a location.
  76.      * @param latitude latitude
  77.      * @param longitude longitude
  78.      * @return modip at specified location, in degrees
  79.      */
  80.     public double computeMODIP(final double latitude, final double longitude) {

  81.         // index in latitude (note that Galileo document uses x for interpolation in latitude)
  82.         // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
  83.         final double x  = (latitude + MathUtils.SEMI_PI) / sizeLat + 1;
  84.         if (x < 1) {
  85.             return -90;
  86.         }
  87.         if (x >= nbCellsLat + 1) {
  88.             return 90;
  89.         }
  90.         final int    i      = (int) FastMath.floor(x);
  91.         final double deltaX = x - i;

  92.         // index in longitude (note that Galileo document uses y for interpolation in longitude)
  93.         // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
  94.         double y  = (longitude + FastMath.PI) / sizeLon + 1;
  95.         while (y < 1) {
  96.             y += nbCellsLon;
  97.         }
  98.         while (y >= nbCellsLon + 1) {
  99.             y -= nbCellsLon;
  100.         }
  101.         final int    j      = (int) FastMath.floor(y);
  102.         final double deltaY = y - j;

  103.         // zi coefficients (Eq. 12 and 13)
  104.         final double z1 = interpolate(grid[i - 1][j - 1], grid[i][j - 1], grid[i + 1][j - 1], grid[i + 2][j - 1], deltaX);
  105.         final double z2 = interpolate(grid[i - 1][j],     grid[i][j],     grid[i + 1][j],     grid[i + 2][j],     deltaX);
  106.         final double z3 = interpolate(grid[i - 1][j + 1], grid[i][j + 1], grid[i + 1][j + 1], grid[i + 2][j + 1], deltaX);
  107.         final double z4 = interpolate(grid[i - 1][j + 2], grid[i][j + 2], grid[i + 1][j + 2], grid[i + 2][j + 2], deltaX);

  108.         // Modip (Ref Eq. 16)
  109.         return interpolate(z1, z2, z3, z4, deltaY);

  110.     }

  111.     /**
  112.      * This method provides a third order interpolation function
  113.      * as recommended in the reference document (Ref Eq. 128 to Eq. 138)
  114.      *
  115.      * @param z1 z1 coefficient
  116.      * @param z2 z2 coefficient
  117.      * @param z3 z3 coefficient
  118.      * @param z4 z4 coefficient
  119.      * @param x position
  120.      * @return a third order interpolation
  121.      */
  122.     private double interpolate(final double z1, final double z2, final double z3, final double z4,
  123.                                final double x) {

  124.         if (x < 5e-11) {
  125.             return z2;
  126.         }

  127.         final double delta = 2.0 * x - 1.0;
  128.         final double g1 = z3 + z2;
  129.         final double g2 = z3 - z2;
  130.         final double g3 = z4 + z1;
  131.         final double g4 = (z4 - z1) / 3.0;
  132.         final double a0 = 9.0 * g1 - g3;
  133.         final double a1 = 9.0 * g2 - g4;
  134.         final double a2 = g3 - g1;
  135.         final double a3 = g4 - g2;
  136.         return 0.0625 * (a0 + delta * (a1 + delta * (a2 + delta * a3)));

  137.     }

  138.     /** Compute modip for a location.
  139.      * @param <T> type of the field elements
  140.      * @param latitude latitude
  141.      * @param longitude longitude
  142.      * @return modip at specified location
  143.      */
  144.     public <T extends CalculusFieldElement<T>> T computeMODIP(final T latitude, final T longitude) {

  145.         // index in latitude (note that Galileo document uses x for interpolation in latitude)
  146.         // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
  147.         final T x  = latitude.add(MathUtils.SEMI_PI).divide(sizeLat).add(1);
  148.         if (x.getReal() < 1) {
  149.             return latitude.newInstance(-90);
  150.         }
  151.         if (x.getReal() >= nbCellsLat + 1) {
  152.             return latitude.newInstance(90);
  153.         }
  154.         final int i      = (int) FastMath.floor(x.getReal());
  155.         final T   deltaX = x.subtract(i);

  156.         // index in longitude (note that Galileo document uses y for interpolation in longitude)
  157.         // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
  158.         T y  = longitude.add(FastMath.PI).divide(sizeLon).add(1);
  159.         while (y.getReal() < 1) {
  160.             y = y.add(nbCellsLon);
  161.         }
  162.         while (y.getReal() >= nbCellsLon + 1) {
  163.             y = y.subtract(nbCellsLon);
  164.         }
  165.         final int j      = (int) FastMath.floor(y.getReal());
  166.         final T   deltaY = y.subtract(j);

  167.         // zi coefficients (Eq. 12 and 13)
  168.         final T z1 = interpolate(x.newInstance(grid[i - 1][j - 1]), x.newInstance(grid[i][j - 1]),
  169.                                  x.newInstance(grid[i + 1][j - 1]), x.newInstance(grid[i + 2][j - 1]),
  170.                                  deltaX);
  171.         final T z2 = interpolate(x.newInstance(grid[i - 1][j]),     x.newInstance(grid[i][j]),
  172.                                  x.newInstance(grid[i + 1][j]),     x.newInstance(grid[i + 2][j]),
  173.                                  deltaX);
  174.         final T z3 = interpolate(x.newInstance(grid[i - 1][j + 1]), x.newInstance(grid[i][j + 1]),
  175.                                  x.newInstance(grid[i + 1][j + 1]), x.newInstance(grid[i + 2][j + 1]),
  176.                                  deltaX);
  177.         final T z4 = interpolate(x.newInstance(grid[i - 1][j + 2]), x.newInstance(grid[i][j + 2]),
  178.                                  x.newInstance(grid[i + 1][j + 2]), x.newInstance(grid[i + 2][j + 2]),
  179.                                  deltaX);

  180.         // Modip (Ref Eq. 16)
  181.         return interpolate(z1, z2, z3, z4, deltaY);

  182.     }

  183.     /**
  184.      * This method provides a third order interpolation function
  185.      * as recommended in the reference document (Ref Eq. 128 to Eq. 138)
  186.      *
  187.      * @param <T> type of the field elements
  188.      * @param z1 z1 coefficient
  189.      * @param z2 z2 coefficient
  190.      * @param z3 z3 coefficient
  191.      * @param z4 z4 coefficient
  192.      * @param x position
  193.      * @return a third order interpolation
  194.      */
  195.     private <T extends CalculusFieldElement<T>> T interpolate(final T z1, final T z2, final T z3, final T z4,
  196.                                                               final T x) {

  197.         if (x.getReal() < 5e-11) {
  198.             return z2;
  199.         }

  200.         final T delta = x.multiply(2.0).subtract(1.0);
  201.         final T g1    = z3.add(z2);
  202.         final T g2    = z3.subtract(z2);
  203.         final T g3    = z4.add(z1);
  204.         final T g4    = z4.subtract(z1).divide(3.0);
  205.         final T a0    = g1.multiply(9.0).subtract(g3);
  206.         final T a1    = g2.multiply(9.0).subtract(g4);
  207.         final T a2    = g3.subtract(g1);
  208.         final T a3    = g4.subtract(g2);
  209.         return delta.multiply(a3).add(a2).multiply(delta).add(a1).multiply(delta).add(a0).multiply(0.0625);

  210.     }

  211.     /**
  212.      * Load data from a stream.
  213.      *
  214.      * @param source grid source
  215.      * @param addWrapping if true, wrapping should be added to loaded data
  216.      */
  217.     private void loadData(final DataSource source, final boolean addWrapping) {
  218.         // if we must add wrapping, we must keep some empty rows and columns that will be filled later on
  219.         final int first = addWrapping ? 1 : 0;

  220.         // Open stream and parse data
  221.         int lineNumber = 0;
  222.         String line = null;
  223.         int row = first;
  224.         try (Reader         r  = source.getOpener().openReaderOnce();
  225.              BufferedReader br = new BufferedReader(r)) {

  226.             for (line = br.readLine(); line != null; line = br.readLine()) {
  227.                 ++lineNumber;

  228.                 // Read grid data
  229.                 final String[] fields = SEPARATOR.split(line.trim());
  230.                 if (fields.length == (addWrapping ? grid[row].length - 2 : grid[row].length)) {
  231.                     // this should be a regular data line (i.e. not a header line)
  232.                     for (int i = 0; i < fields.length; i++) {
  233.                         grid[row][first + i] = Double.parseDouble(fields[i]);
  234.                     }
  235.                     ++row;
  236.                 }

  237.             }

  238.         } catch (IOException | NumberFormatException e) {
  239.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, source.getName(), line);
  240.         }

  241.         if (addWrapping) {

  242.             // we must add rows phased 180° in longitude after poles, for the sake of interpolation
  243.             final double[] extraNorth = grid[0];
  244.             final double[] refNorth   = grid[2];
  245.             final double[] extraSouth = grid[grid.length - 1];
  246.             final double[] refSouth   = grid[grid.length - 3];
  247.             final int      deltaPhase = nbCellsLon / 2;
  248.             for (int j = 1; j <= deltaPhase; j++) {
  249.                 extraNorth[j]              = refNorth[j + deltaPhase];
  250.                 extraNorth[j + deltaPhase] = refNorth[j];
  251.                 extraSouth[j]              = refSouth[j + deltaPhase];
  252.                 extraSouth[j + deltaPhase] = refSouth[j];
  253.             }
  254.             extraNorth[nbCellsLon + 1] = extraNorth[1];
  255.             extraSouth[nbCellsLon + 1] = extraSouth[1];

  256.             // we must copy columns to wrap around Earth in longitude
  257.             // the first three columns must be identical to the last three columns
  258.             // the columns 1 and gi.length - 2 (i.e. anti-meridian) are already
  259.             // identical in the original resource file
  260.             for (final double[] gi : grid) {
  261.                 gi[0] = gi[gi.length - 3];
  262.                 gi[gi.length - 1] = gi[2];
  263.             }

  264.             row++;

  265.         }

  266.         // Throw an exception if modip grid was not loaded properly
  267.         if (row != grid.length) {
  268.             throw new OrekitException(OrekitMessages.MODIP_GRID_NOT_LOADED, source.getName());
  269.         }

  270.     }

  271. }