AbstractJacchiaBowmanModel.java

/* Copyright 2002-2025 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.models.earth.atmosphere;

import java.util.stream.IntStream;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathArrays;
import org.hipparchus.util.MathUtils;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.FieldGeodeticPoint;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.time.TimeScale;
import org.orekit.utils.Constants;
import org.orekit.utils.ExtendedPositionProvider;

/**
 * Base class for Jacchia-Bowman atmospheric models.
 * @see JB2006
 * @see JB2008
 * @author Bruce R Bowman (HQ AFSPC, Space Analysis Division), Feb 2006: FORTRAN routine
 * @author Fabien Maussion (java translation)
 * @author Bryan Cazabonne (field elements translation)
 * @since 13.1
 */
public abstract class AbstractJacchiaBowmanModel extends AbstractSunInfluencedAtmosphere {

    /** Minimum altitude (m) for JB models use. */
    private static final double ALT_MIN = 90000.;

    /** The alpha are the thermal diffusion coefficients in equation (6). */
    private static final double[] ALPHA = {0, 0, 0, 0, -0.38};

    /** Natural logarithm of 10.0. */
    private static final double LOG10  = FastMath.log(10.);

    /** Molecular weights in order: N2, O2, O, Ar, He and H. */
    private static final double[] AMW = {28.0134, 31.9988, 15.9994, 39.9480, 4.0026, 1.00797};

    /** Avogadro's number in mks units (molecules/kmol). */
    private static final double AVOGAD = 6.02257e26;

    /** The FRAC are the assumed sea-level volume fractions in order: N2, O2, Ar, and He. */
    private static final double[] FRAC = {0.78110, 0.20955, 9.3400e-3, 1.2890e-5};

    /** Universal gas-constant in mks units (joules/K/kmol). */
    private static final double RSTAR  = 8.31432;

    /** Value used to establish height step sizes in the regime 90km to 105km. */
    private static final double R1 = 0.010;

    /** Value used to establish height step sizes in the regime 105km to 500km. */
    private static final double R2 = 0.025;

    /** Value used to establish height step sizes in the regime above 500km. */
    private static final double R3 = 0.075;

    /** Weights for the Newton-Cotes five-points quadrature formula. */
    private static final double[] WT = {0.311111111111111, 1.422222222222222, 0.533333333333333, 1.422222222222222, 0.311111111111111};

    /** Earth radius (km). */
    private static final double EARTH_RADIUS = 6356.766;

    /** DTC relative data. */
    private static final double[] BDT_SUB = {-0.457512297e+01, -0.512114909e+01, -0.693003609e+02,
                                             0.203716701e+03, 0.703316291e+03, -0.194349234e+04,
                                             0.110651308e+04, -0.174378996e+03, 0.188594601e+04,
                                             -0.709371517e+04, 0.922454523e+04, -0.384508073e+04,
                                             -0.645841789e+01, 0.409703319e+02, -0.482006560e+03,
                                             0.181870931e+04, -0.237389204e+04, 0.996703815e+03,
                                             0.361416936e+02};

    /** DTC relative data.  */
    private static final double[] CDT_SUB = {-0.155986211e+02, -0.512114909e+01, -0.693003609e+02,
                                             0.203716701e+03, 0.703316291e+03, -0.194349234e+04,
                                             0.110651308e+04, -0.220835117e+03, 0.143256989e+04,
                                             -0.318481844e+04, 0.328981513e+04, -0.135332119e+04,
                                             0.199956489e+02, -0.127093998e+02, 0.212825156e+02,
                                             -0.275555432e+01, 0.110234982e+02, 0.148881951e+03,
                                             -0.751640284e+03, 0.637876542e+03, 0.127093998e+02,
                                             -0.212825156e+02, 0.275555432e+01};

    /** Mbar polynomial coeffcients. */
    private static final double[] CXAMB = {28.15204, -8.5586e-2, +1.2840e-4, -1.0056e-5, -1.0210e-5, +1.5044e-6, +9.9826e-8};

    /** Coefficients for high altitude density correction. */
    private static final double[] CHT = {0.22, -0.20e-02, 0.115e-02, -0.211e-05};

    /** UTC time scale. */
    private final TimeScale utc;

    /** Earth body shape. */
    private final BodyShape earth;

    /** Earliest epoch of solar activity data. */
    private final AbsoluteDate minDataEpoch;

    /** Latest epoch of solar activity data. */
    private final AbsoluteDate maxDataEpoch;

    /**
     * Constructor.
     *
     * @param sun          position provider.
     * @param utc          UTC time scale. Used to computed the day fraction.
     * @param earth        the earth body shape
     * @param minDataEpoch earliest epoch of solar activity data
     * @param maxDataEpoch latest epoch of solar activity data
     */
    protected AbstractJacchiaBowmanModel(final ExtendedPositionProvider sun, final TimeScale utc, final BodyShape earth,
                                         final AbsoluteDate minDataEpoch, final AbsoluteDate maxDataEpoch) {
        super(sun);
        this.utc          = utc;
        this.earth        = earth;
        this.minDataEpoch = minDataEpoch;
        this.maxDataEpoch = maxDataEpoch;
    }

    /** Get the UTC time scale.
     * @return UTC time scale
     */
    public TimeScale getUtc() {
        return utc;
    }

    /** Get the Earth body shape.
     * @return the Earth body shape
     */
    public BodyShape getEarth() {
        return earth;
    }

    /** {@inheritDoc}*/
    @Override
    public double getDensity(final AbsoluteDate date, final Vector3D position, final Frame frame) {

        // Verify availability of data
        if (date.compareTo(maxDataEpoch) > 0 || date.compareTo(minDataEpoch) < 0) {
            throw new OrekitException(OrekitMessages.NO_SOLAR_ACTIVITY_AT_DATE, date, minDataEpoch, maxDataEpoch);
        }

        // compute geodetic position
        final GeodeticPoint inBody = earth.transform(position, frame, date);

        // compute sun position
        final Frame ecef = getFrame();
        final Vector3D sunPos = getSunPosition(date, ecef);
        final GeodeticPoint sunInBody = earth.transform(sunPos, ecef, date);
        return computeDensity(date, sunInBody.getLongitude(), sunInBody.getLatitude(), inBody.getLongitude(), inBody.getLatitude(), inBody.getAltitude());
    }

    /** {@inheritDoc}*/
    @Override
    public <T extends CalculusFieldElement<T>> T getDensity(final FieldAbsoluteDate<T> date, final FieldVector3D<T> position, final Frame frame) {

        // Verify availability of data
        final AbsoluteDate dateD = date.toAbsoluteDate();
        if (dateD.compareTo(maxDataEpoch) > 0 || dateD.compareTo(minDataEpoch) < 0) {
            throw new OrekitException(OrekitMessages.NO_SOLAR_ACTIVITY_AT_DATE, dateD, minDataEpoch, maxDataEpoch);
        }

        // compute geodetic position (km and °)
        final FieldGeodeticPoint<T> inBody = earth.transform(position, frame, date);

        // compute sun position
        final Frame ecef = getFrame();
        final FieldVector3D<T> sunPos = getSunPosition(date, ecef);
        final FieldGeodeticPoint<T> sunInBody = earth.transform(sunPos, ecef, date);
        return computeDensity(date,
                              sunInBody.getLongitude(), sunInBody.getLatitude(),
                              inBody.getLongitude(), inBody.getLatitude(), inBody.getAltitude());
    }

    /** {@inheritDoc} */
    @Override
    public Frame getFrame() {
        return earth.getBodyFrame();
    }

    /** Computes the local density with initial entries.
     * @param date computation epoch
     * @param sunRA Right Ascension of Sun (radians)
     * @param sunDecli Declination of Sun (radians)
     * @param satLon Right Ascension of position (radians)
     * @param satLat Geocentric latitude of position (radians)
     * @param satAlt Height of position (m)
     * @return total mass-Density at input position (kg/m³)
     */
    protected double computeDensity(final AbsoluteDate date,
                                    final double sunRA, final double sunDecli,
                                    final double satLon, final double satLat, final double satAlt) {

        if (satAlt < ALT_MIN) {
            throw new OrekitException(OrekitMessages.ALTITUDE_BELOW_ALLOWED_THRESHOLD, satAlt, ALT_MIN);
        }
        final double altKm = satAlt / 1000.0;

        final DateTimeComponents dt = date.getComponents(utc);
        final double dateMJD = dt.getDate().getMJD() +
                               dt.getTime().getSecondsInLocalDay() / Constants.JULIAN_DAY;

        // Equation (14)
        // Temperature equation obtained using numerous satellites for the years from 1996 through 2004 when all new solar indices were available
        final double tsubc = computeTc(date);

        // Equation (15)
        final double eta = 0.5 * FastMath.abs(satLat - sunDecli);
        final double theta = 0.5 * FastMath.abs(satLat + sunDecli);

        // Equation (16)
        final double h   = satLon - sunRA;
        final double tau = h - 0.64577182 + 0.10471976 * FastMath.sin(h + 0.75049158);
        final double solarTime = solarTimeHour(h);

        // Equation (17)
        final double tsubl = tSubL(eta, theta, tau, tsubc);

        // Compute correction to dTc for local solar time and lat correction
        final double dtclst = dTc(getF10(date), solarTime, satLat, altKm);

        // Compute the local exospheric temperature.
        final double tInf = computeTInf(date, tsubl, dtclst);

        // Equation (9)
        final double tsubx = tSubX(tInf);

        // Equation (11)
        final double gsubx = gSubX(tsubx);

        // The TC array will be an argument in the call to "localTemp"
        final double[] tc = tSubCArray(tsubx, gsubx, tInf);

        // Equation (5)
        final double z1    = 90.;
        final double z2    = FastMath.min(altKm, 105.0);
        double al          = FastMath.log(z2 / z1);
        int n              = (int) FastMath.floor(al / R1) + 1;
        double zr          = FastMath.exp(al / n);
        final double mb1   = mBar(z1);
        final double tloc1 = localTemp(z1, tc);
        double zend        = z1;
        double sub2        = 0.;
        double ain         = mb1 * gravity(z1) / tloc1;
        double mb2         = 0;
        double tloc2       = 0;
        double z           = 0;
        double gravl       = 0;

        for (int i = 0; i < n; ++i) {
            z = zend;
            zend = zr * z;
            final double dz = 0.25 * (zend - z);
            double sum1 = WT[0] * ain;
            for (int j = 1; j < 5; ++j) {
                z += dz;
                mb2   = mBar(z);
                tloc2 = localTemp(z, tc);
                gravl = gravity(z);
                ain   = mb2 * gravl / tloc2;
                sum1 += WT[j] * ain;
            }
            sub2 += dz * sum1;
        }
        double rho = 3.46e-6 * mb2 * tloc1 / FastMath.exp(sub2 / RSTAR) / (mb1 * tloc2);

        // Equation (2)
        final double anm = AVOGAD * rho;
        final double an = anm / mb2;

        // Equation (3)
        double fact2 = anm / 28.960;
        final double[] aln = new double[6];
        aln[0] = FastMath.log(FRAC[0] * fact2);
        aln[3] = FastMath.log(FRAC[2] * fact2);
        aln[4] = FastMath.log(FRAC[3] * fact2);
        // Equation (4)
        aln[1] = FastMath.log(fact2 * (1. + FRAC[1]) - an);
        aln[2] = FastMath.log(2. * (an - fact2));

        if (altKm <= 105.0) {
            // Put in negligible hydrogen for use in DO-LOOP 13
            aln[5] = aln[4] - 25.0;
        }
        else {
            // Equation (6)
            al = FastMath.log(FastMath.min(altKm, 500.0) / z);
            n = 1 + (int) FastMath.floor(al / R2);
            zr = FastMath.exp(al / n);
            sub2 = 0.;
            ain = gravl / tloc2;

            double tloc3 = 0;
            for (int i = 0; i < n; ++i) {
                z = zend;
                zend = zr * z;
                final double dz = 0.25 * (zend - z);
                double SUM1 = WT[0] * ain;
                for (int j = 1; j < 5; ++j) {
                    z += dz;
                    tloc3 = localTemp(z, tc);
                    gravl = gravity(z);
                    ain = gravl / tloc3;
                    SUM1 += WT[j] * ain;
                }
                sub2 += dz * SUM1;
            }

            al = FastMath.log(FastMath.max(altKm, 500.0) / z);
            final double r = (altKm > 500.0) ? R3 : R2;
            n = 1 + (int) FastMath.floor(al / r);
            zr = FastMath.exp(al / n);
            double sum3 = 0.;
            double tloc4 = 0;
            for (int i = 0; i < n; ++i) {
                z = zend;
                zend = zr * z;
                final double dz = 0.25 * (zend - z);
                double sum1 = WT[0] * ain;
                for (int j = 1; j < 5; ++j) {
                    z += dz;
                    tloc4 = localTemp(z, tc);
                    gravl = gravity(z);
                    ain = gravl / tloc4;
                    sum1 = sum1 + WT[j] * ain;
                }
                sum3 = sum3 + dz * sum1;
            }
            final double altr;
            final double hSign;
            if (altKm <= 500.) {
                altr = FastMath.log(tloc3 / tloc2);
                fact2 = sub2 / RSTAR;
                hSign = 1.0;
            }
            else {
                altr = FastMath.log(tloc4 / tloc2);
                fact2 = (sub2 + sum3) / RSTAR;
                hSign = -1.0;
            }
            for (int i = 0; i < 5; ++i) {
                aln[i] = aln[i] - (1.0 + ALPHA[i]) * altr - fact2 * AMW[i];
            }

            // Equation (7) - Note that in CIRA72, AL10T5 = DLOG10(T500)
            final double al10t5 = FastMath.log10(tInf);
            final double alnh5 = (5.5 * al10t5 - 39.40) * al10t5 + 73.13;
            aln[5] = LOG10 * (alnh5 + 6.) + hSign * (FastMath.log(tloc4 / tloc3) + sum3 * AMW[5] / RSTAR);
        }

        // Equation (24)  - J70 Seasonal-Latitudinal Variation
        final double dlrsl = dlrsl(altKm, dateMJD, satLat);

        // Equation (23) - Computes the semiannual variation
        double dlrsa = 0;
        if (z < 2000.0) {
            // Use new semiannual model DELTA LOG RHO
            dlrsa = semian(date, dayOfYear(dateMJD), altKm);
        }

        // Sum the delta-log-rhos and apply to the number densities.
        // In CIRA72 the following equation contains an actual sum,
        // namely DLR = AL10 * (DLRGM + DLRSA + DLRSL)
        // However, for Jacchia 70, there is no DLRGM or DLRSA.
        final double dlr = LOG10 * (dlrsl + dlrsa);
        for (int i = 0; i < 6; ++i) {
            aln[i] += dlr;
        }

        // Compute mass-density and mean-molecular-weight and
        // convert number density logs from natural to common.
        final double sumnm = IntStream.range(0, 6).mapToDouble(i -> FastMath.exp(aln[i]) * AMW[i]).sum();
        rho = sumnm / AVOGAD;

        // Compute the high altitude exospheric density correction factor
        final double fex = densityCorrectionFactor(altKm, getF10B(date));

        // Apply the exospheric density correction factor.
        rho *= fex;

        return rho;
    }

    /** Computes the local density with initial entries.
     * @param date computation epoch
     * @param sunRA Right Ascension of Sun (radians)
     * @param sunDecli Declination of Sun (radians)
     * @param satLon Right Ascension of position (radians)
     * @param satLat Geocentric latitude of position (radians)
     * @param satAlt Height of position (m)
     * @param <T> type of the elements
     * @return total mass-Density at input position (kg/m³)
     */
    protected <T extends CalculusFieldElement<T>> T computeDensity(final FieldAbsoluteDate<T> date,
                                                                   final T sunRA, final T sunDecli,
                                                                   final T satLon, final T satLat, final T satAlt) {

        if (satAlt.getReal() < ALT_MIN) {
            throw new OrekitException(OrekitMessages.ALTITUDE_BELOW_ALLOWED_THRESHOLD, satAlt, ALT_MIN);
        }

        final AbsoluteDate dateR = date.toAbsoluteDate();
        final DateTimeComponents components = date.getComponents(utc);
        final T dateMJD = date.durationFrom(new FieldAbsoluteDate<>(date.getField(), components, utc))
                              .add(components.getTime().getSecondsInLocalDay())
                              .divide(Constants.JULIAN_DAY)
                              .add(components.getDate().getMJD());

        final Field<T> field  = satAlt.getField();
        final T altKm = satAlt.divide(1000.0);

        // Equation (14) (Temperature equation)
        final double tsubc = computeTc(dateR);

        // Equation (15)
        final T eta   = FastMath.abs(satLat.subtract(sunDecli)).multiply(0.5);
        final T theta = FastMath.abs(satLat.add(sunDecli)).multiply(0.5);

        // Equation (16)
        final T h = satLon.subtract(sunRA);
        final T tau = h.subtract(0.64577182).add(h.add(0.75049158).sin().multiply(0.10471976));
        final T solarTime = solarTimeHour(h);

        // Equation (17)
        final T tsubl = tSubL(eta, theta, tau, tsubc);

        // Compute correction to dTc for local solar time and lat correction
        final T dtclst = dTc(getF10(dateR), solarTime, satLat, altKm);

        // Compute the local exospheric temperature.
        final T tinf = computeTInf(dateR, tsubl, dtclst);

        // Equation (9)
        final T tsubx = tSubX(tinf);

        // Equation (11)
        final T gsubx = gSubX(tsubx);

        // The TC array will be an argument in the call to "localTemp"
        final T[] tc = tSubCArray(tsubx, gsubx, tinf, field);

        // Equation (5)
        final T z1    = field.getZero().newInstance(90.);
        final T z2    = FastMath.min(altKm, 105.0);
        T al          = z2.divide(z1).log();
        int n         = 1 + (int) FastMath.floor(al.getReal() / R1);
        T zr          = al.divide(n).exp();
        final T mb1   = mBar(z1);
        final T tloc1 = localTemp(z1, tc);
        T zend        = z1;
        T sub2        = field.getZero();
        T ain         = mb1.multiply(gravity(z1)).divide(tloc1);
        T mb2         = field.getZero();
        T tloc2       = field.getZero();
        T z           = field.getZero();
        T gravl       = field.getZero();

        for (int i = 0; i < n; ++i) {
            z = zend;
            zend = zr.multiply(z);
            final T dz = zend.subtract(z).multiply(0.25);
            T sum1 = ain.multiply(WT[0]);
            for (int j = 1; j < 5; ++j) {
                z = z.add(dz);
                mb2   = mBar(z);
                tloc2 = localTemp(z, tc);
                gravl = gravity(z);
                ain   = mb2.multiply(gravl).divide(tloc2);
                sum1  = sum1.add(ain.multiply(WT[j]));
            }
            sub2 = sub2.add(dz.multiply(sum1));
        }
        T rho = mb2.multiply(3.46e-6).multiply(tloc1).divide(sub2.divide(RSTAR).exp().multiply(mb1.multiply(tloc2)));

        // Equation (2)
        final T anm = rho.multiply(AVOGAD);
        final T an  = anm.divide(mb2);

        // Equation (3)
        T fact2  = anm.divide(28.960);
        final T[] aln = MathArrays.buildArray(field, 6);
        aln[0] = fact2.multiply(FRAC[0]).log();
        aln[3] = fact2.multiply(FRAC[2]).log();
        aln[4] = fact2.multiply(FRAC[3]).log();

        // Equation (4)
        aln[1] = fact2.multiply(1. + FRAC[1]).subtract(an).log();
        aln[2] = an.subtract(fact2).multiply(2).log();

        if (altKm.getReal() <= 105.0) {
            // Put in negligible hydrogen for use in DO-LOOP 13
            aln[5] = aln[4].subtract(25.0);
        }
        else {
            // Equation (6)
            al   = FastMath.min(altKm, 500.0).divide(z).log();
            n    = 1 + (int) FastMath.floor(al.getReal() / R2);
            zr   = al.divide(n).exp();
            sub2 = field.getZero();
            ain  = gravl.divide(tloc2);

            T tloc3 = field.getZero();
            for (int i = 0; i < n; ++i) {
                z = zend;
                zend = zr.multiply(z);
                final T dz = zend.subtract(z).multiply(0.25);
                T sum1 = ain.multiply(WT[0]);
                for (int j = 1; j < 5; ++j) {
                    z     = z.add(dz);
                    tloc3 = localTemp(z, tc);
                    gravl = gravity(z);
                    ain   = gravl.divide(tloc3);
                    sum1  = sum1.add(ain.multiply(WT[j]));
                }
                sub2 = sub2.add(dz.multiply(sum1));
            }

            al = FastMath.max(altKm, 500.0).divide(z).log();
            final double r = (altKm.getReal() > 500.0) ? R3 : R2;
            n = 1 + (int) FastMath.floor(al.getReal() / r);
            zr = al.divide(n).exp();
            T sum3 = field.getZero();
            T tloc4 = field.getZero();
            for (int i = 0; i < n; ++i) {
                z = zend;
                zend = zr.multiply(z);
                final T dz = zend.subtract(z).multiply(0.25);
                T sum1 = ain.multiply(WT[0]);
                for (int j = 1; j < 5; ++j) {
                    z = z.add(dz);
                    tloc4 = localTemp(z, tc);
                    gravl = gravity(z);
                    ain   = gravl.divide(tloc4);
                    sum1  = sum1.add(ain.multiply(WT[j]));
                }
                sum3 = sum3.add(dz.multiply(sum1));
            }
            final T altr;
            final double hSign;
            if (altKm.getReal() <= 500.) {
                altr = tloc3.divide(tloc2).log();
                fact2 = sub2.divide(RSTAR);
                hSign = 1.0;
            }
            else {
                altr = tloc4.divide(tloc2).log();
                fact2 = sub2.add(sum3).divide(RSTAR);
                hSign = -1.0;
            }
            for (int i = 0; i < 5; ++i) {
                aln[i] = aln[i].subtract(altr.multiply(1.0 + ALPHA[i])).subtract(fact2.multiply(AMW[i]));
            }

            // Equation (7) - Note that in CIRA72, AL10T5 = DLOG10(T500)
            final T al10t5 = tinf.log10();
            final T alnh5 = al10t5.multiply(5.5).subtract(39.40).multiply(al10t5).add(73.13);
            aln[5] = alnh5.add(6.).multiply(LOG10).add(tloc4.divide(tloc3).log().add(sum3.multiply(AMW[5] / RSTAR)).multiply(hSign));
        }

        // Equation (24)  - J70 Seasonal-Latitudinal Variation
        final T dlrsl = dlrsl(altKm, dateMJD, satLat);

        // Equation (23) - Computes the semiannual variation
        T dlrsa = field.getZero();
        if (z.getReal() < 2000.0) {
            // Use new semiannual model DELTA LOG RHO
            dlrsa = semian(dateR, dayOfYear(dateMJD), altKm);
        }

        // Sum the delta-log-rhos and apply to the number densities.
        // In CIRA72 the following equation contains an actual sum,
        // namely DLR = AL10 * (DLRGM + DLRSA + DLRSL)
        // However, for Jacchia 70, there is no DLRGM or DLRSA.
        final T dlr = dlrsl.add(dlrsa).multiply(LOG10);
        for (int i = 0; i < 6; ++i) {
            aln[i] = aln[i].add(dlr);
        }

        // Compute mass-density and mean-molecular-weight and
        // convert number density logs from natural to common.
        T sumnm = field.getZero();
        for (int i = 0; i < 6; ++i) {
            sumnm = sumnm.add(aln[i].exp().multiply(AMW[i]));
        }
        rho = sumnm.divide(AVOGAD);

        // Compute the high altitude exospheric density correction factor
        final T fex = densityCorrectionFactor(altKm, getF10B(dateR), field);

        // Apply the exospheric density correction factor.
        rho = rho.multiply(fex);

        return rho;
    }

    /** Computes the semi-annual variation (delta log(rho)).
     * @param date computation epoch
     * @param day day of year
     * @param altKm height (km)
     * @return semi-annual variation
     */
    protected abstract double semian(AbsoluteDate date, double day, double altKm);

    /**
     * Computes the local exospheric temperature.
     * @param date computation epoch
     * @param tsubl exospheric temperature ("tsubl" term), given by Equation (17)
     * @param dtclst correction to dTc for local solar time and lat correction
     * @return the local exospheric temperature
     */
    protected abstract double computeTInf(AbsoluteDate date, double tsubl, double dtclst);

    /** Computes the temperature equation.
     * @param date computation epoch
     * @return the temperature equation
     */
    protected abstract double computeTc(AbsoluteDate date);

    /** Get the 10.7-cm Solar flux (1e<sup>-22</sup>*Watt/(m²*Hertz))<br> (Tabular time 1.0 day earlier).
     * @param date computation epoch
     * @return the 10.7-cm Solar flux from model input parameters
     */
    protected abstract double getF10(AbsoluteDate date);

    /** Get the 10.7-cm Solar Flux, averaged 81-day centered on the input time<br> (Tabular time 1.0 day earlier).
     * @param date computation epoch
     * @return the 10.7-cm Solar Flux, averaged 81-day centered on the input time
     */
    protected abstract double getF10B(AbsoluteDate date);

    /** Computes the semi-annual variation (delta log(rho)).
     * @param date computation epoch
     * @param day day of year
     * @param altKm height (km)
     * @param <T> type of the elements
     * @return semi-annual variation
     */
    protected abstract <T extends CalculusFieldElement<T>> T semian(AbsoluteDate date, T day, T altKm);

    /**
     * Computes the local exospheric temperature.
     * @param date computation epoch
     * @param tsubl exospheric temperature ("tsubl" term), given by Equation (17)
     * @param dtclst correction to dTc for local solar time and lat correction
     * @param <T> type of the elements
     * @return the local exospheric temperature
     */
    protected abstract <T extends CalculusFieldElement<T>> T computeTInf(AbsoluteDate date, T tsubl, T dtclst);

    /** Evaluates the solar time in hours.
     * @param h difference between the Right Ascension of position and the Right Ascension of Sun (radians)
     * @return the solar time in hours
     */
    private static double solarTimeHour(final double h) {
        final double solTimeHour = FastMath.toDegrees(h + FastMath.PI) / 15.0;
        if (solTimeHour >= 24) {
            return solTimeHour - 24.;
        }
        if (solTimeHour < 0) {
            return solTimeHour + 24.;
        }
        return solTimeHour;
    }

    /** Evaluates the solar time in hours.
     * @param h difference between the Right Ascension of position and the Right Ascension of Sun (radians)
     * @param <T> type of the field elements
     * @return the solar time in hours
     */
    private static <T extends CalculusFieldElement<T>> T solarTimeHour(final T h) {
        final T solarTime = FastMath.toDegrees(h.add(FastMath.PI)).divide(15.0);
        if (solarTime.getReal() >= 24) {
            return solarTime.subtract(24);
        }
        if (solarTime.getReal() < 0) {
            return solarTime.add(24);
        }
        return solarTime;
    }

    /** Evaluates the exospheric temperature ("tsubl" term), Equation (17).
     * <p>
     * This temperature corresponds to the exospheric temperature in low
     * geomagnetic conditions.
     * </p>
     * @param eta "eta" term, Equation (15)
     * @param theta "theta" term, Equation (15)
     * @param tau "tau" term, Equation (16)
     * @param tsubc exospheric temperature Tc, Equation (14)
     * @return Tl temperature computed by Equation (17)
     */
    private static double tSubL(final double eta, final double theta,
                               final double tau, final double tsubc) {
        final double cosEta   = FastMath.pow(FastMath.cos(eta), 2.5);
        final double sinTheta = FastMath.pow(FastMath.sin(theta), 2.5);
        final double cosTau   = FastMath.abs(FastMath.cos(0.5 * tau));
        final double df       = sinTheta + (cosEta - sinTheta) * cosTau * cosTau * cosTau;
        return tsubc * (1. + 0.31 * df);
    }

    /** Evaluates exospheric temperature ("tsubl" term), Equation (17).
     * <p>
     * This temperature corresponds to the exospheric temperature in low
     * geomagnetic conditions.
     * </p>
     * @param eta "eta" term, Equation (15)
     * @param theta "theta" term, Equation (15)
     * @param tau "tau" term, Equation (16)
     * @param tsubc exospheric temperature Tc, Equation (14)
     * @param <T> type of the field elements
     * @return Tl temperature computed by Equation (17)
     */
    private static <T extends CalculusFieldElement<T>>  T tSubL(final T eta, final T theta,
                                                               final T tau, final double tsubc) {
        final T cos      = eta.cos();
        final T cosEta   = cos.square().multiply(cos.sqrt());
        final T sin      = theta.sin();
        final T sinTheta = sin.square().multiply(sin.sqrt());
        final T cosTau   = tau.multiply(0.5).cos().abs();
        final T df       = sinTheta.add(cosEta.subtract(sinTheta).multiply(cosTau).multiply(cosTau).multiply(cosTau));
        return df.multiply(0.31).add(1).multiply(tsubc);
    }

    /** Evaluates the inflection temperature ("tsubx" term), Equation (9).
     * <p>
     * This temperature corresponds to the temperature at the inflexion altitude.
     * At this altitude, the temperature profile has an inflection point.
     * </p>
     * @param tInf local exospheric temperature
     * @return Tx temperature at inflection point, computed by Equation (9)
     */
    private static double tSubX(final double tInf) {
        return 444.3807 + 0.02385 * tInf - 392.8292 * FastMath.exp(-0.0021357 * tInf);
    }

    /** Evaluates the inflection temperature ("tsubx" term), Equation (9).
     * <p>
     * This temperature corresponds to the temperature at the inflexion altitude.
     * At this altitude, the temperature profile has an inflection point.
     * </p>
     * @param tInf local exospheric temperature
     * @param <T> type of the field elements
     * @return Tx temperature at inflection point, computed by Equation (9)
     */
    private static <T extends CalculusFieldElement<T>>  T tSubX(final T tInf) {
        return tInf.multiply(0.02385).add(444.3807).subtract(tInf.multiply(-0.0021357).exp().multiply(392.8292));
    }

    /** Evaluates the temperature gradient at the inflection point ("gsubx" term), Equation (11).
     * @param tSubX temperature at inflection point
     * @return the temperature gradient at the inflection point computed by Equation (11)
     */
    private static double gSubX(final double tSubX) {
        return 0.054285714 * (tSubX - 183.);
    }

    /** Evaluates the temperature gradient at the inflection point ("gsubx" term), Equation (11).
     * @param tSubX temperature at inflection point
     * @param <T> type of the field elements
     * @return the temperature gradient at the inflection point computed by Equation (11)
     */
    private static <T extends CalculusFieldElement<T>> T gSubX(final T tSubX) {
        return tSubX.subtract(183.).multiply(0.054285714);
    }

    /** Evaluates the Tc array.
     * <p>
     * The Tc array will be used to evaluates {@link #localTemp(double, double[])}.
     * </p>
     * @param tSubX temperature at inflection point
     * @param gSubX the temperature gradient at the inflection point
     * @param tInf local exospheric temperature
     * @return the Tc array
     */
    private static double[] tSubCArray(final double tSubX, final double gSubX,
                                      final double tInf) {
        final double[] tc = new double[4];
        tc[0] = tSubX;
        tc[1] = gSubX;
        tc[2] = (tInf - tSubX) / MathUtils.SEMI_PI;
        tc[3] = gSubX / tc[2];
        return tc;
    }

    /** Evaluates the Tc array.
     * <p>
     * The Tc array will be used to evaluates {@link #localTemp(CalculusFieldElement, CalculusFieldElement[])}
     * </p>
     * @param tSubX temperature at inflection point
     * @param gSubX the temperature gradient at the inflection point
     * @param tInf local exospheric temperature
     * @param field field for the elements
     * @param <T> type of the field elements
     * @return the Tc array
     */
    private static <T extends CalculusFieldElement<T>> T[] tSubCArray(final T tSubX, final T gSubX,
                                                                     final T tInf, final Field<T> field) {
        final T[] tc = MathArrays.buildArray(field, 4);
        tc[0] = tSubX;
        tc[1] = gSubX;
        tc[2] = tInf.subtract(tSubX).divide(MathUtils.SEMI_PI);
        tc[3] = gSubX.divide(tc[2]);
        return tc;
    }

    /** Evaluates the local temperature, Equation (10) or (13) depending on altitude.
     * @param z  altitude
     * @param tc tc array
     * @return temperature profile
     */
    private static double localTemp(final double z, final double[] tc) {
        final double dz = z - 125;
        if (dz <= 0) {
            return ((-9.8204695e-6 * dz - 7.3039742e-4) * dz * dz + 1.0) * dz * tc[1] + tc[0];
        }
        else {
            return tc[0] + tc[2] * FastMath.atan(tc[3] * dz * (1 + 4.5e-6 * FastMath.pow(dz, 2.5)));
        }
    }

    /** Evaluates the local temperature, Equation (10) or (13) depending on altitude.
     * @param z altitude
     * @param tc tc array
     * @return temperature profile
     * @param <T> type of the field elements
     */
    private static <T extends CalculusFieldElement<T>> T localTemp(final T z, final T[] tc) {
        final T dz = z.subtract(125.);
        if (dz.getReal() <= 0.) {
            return dz.multiply(-9.8204695e-6).subtract(7.3039742e-4).multiply(dz).multiply(dz).add(1.0).multiply(dz).multiply(tc[1]).add(tc[0]);
        } else {
            return dz.multiply(dz).multiply(dz.sqrt()).multiply(4.5e-6).add(1).multiply(dz).multiply(tc[3]).atan().multiply(tc[2]).add(tc[0]);
        }
    }

    /** Evaluates mean molecualr mass, Equation (1).
     * @param z altitude (km)
     * @return mean molecular mass
     */
    private static double mBar(final double z) {
        final double dz = z - 100.;
        double amb = CXAMB[6];
        for (int i = 5; i >= 0; --i) {
            amb = dz * amb + CXAMB[i];
        }
        return amb;
    }

    /** Evaluates mean molecualr mass, Equation (1).
     * @param z altitude (km)
     * @return mean molecular mass
     * @param <T> type of the field elements
     */
    private static <T extends CalculusFieldElement<T>>  T mBar(final T z) {
        final T dz = z.subtract(100.);
        T amb = z.getField().getZero().newInstance(CXAMB[6]);
        for (int i = 5; i >= 0; --i) {
            amb = dz.multiply(amb).add(CXAMB[i]);
        }
        return amb;
    }

    /** Evaluates the gravity at the altitude, Equation (8).
     * @param z altitude (km)
     * @return the gravity field (m/s2)
     */
    private static double gravity(final double z) {
        final double temp = 1.0 + z / EARTH_RADIUS;
        return Constants.G0_STANDARD_GRAVITY / (temp * temp);
    }

    /** Evaluates the gravity at the altitude, Equation (8).
     * @param z altitude (km)
     * @return the gravity (m/s2)
     * @param <T> type of the field elements
     */
    private static <T extends CalculusFieldElement<T>> T gravity(final T z) {
        final T tmp = z.divide(EARTH_RADIUS).add(1);
        return tmp.multiply(tmp).reciprocal().multiply(Constants.G0_STANDARD_GRAVITY);
    }

    /** Compute day of year.
     * @param dateMJD Modified Julian date
     * @return the number days in year
     */
    private static double dayOfYear(final double dateMJD) {
        final double d1950 = dateMJD - 33281;

        int iyday = (int) d1950;
        final double frac = d1950 - iyday;
        iyday = iyday + 364;

        int itemp = iyday / 1461;

        iyday = iyday - itemp * 1461;
        itemp = iyday / 365;
        if (itemp >= 3) {
            itemp = 3;
        }
        iyday = iyday - 365 * itemp + 1;
        return iyday + frac;
    }

    /** Compute day of year.
     * @param dateMJD Modified Julian date
     * @param <T> type of the field elements
     * @return the number days in year
     */
    private static <T extends CalculusFieldElement<T>> T dayOfYear(final T dateMJD) {
        final T d1950 = dateMJD.subtract(33281);

        int iyday = (int) d1950.getReal();
        final T frac = d1950.subtract(iyday);
        iyday = iyday + 364;

        int itemp = iyday / 1461;

        iyday = iyday - itemp * 1461;
        itemp = iyday / 365;
        if (itemp >= 3) {
            itemp = 3;
        }
        iyday = iyday - 365 * itemp + 1;
        return frac.add(iyday);
    }

    /** Evaluates the J70 Seasonal-Latitudinal Variation, Equation (24).
     * @param altKm satellite altitude (km)
     * @param dateMJD Modified Julian date
     * @param satLat Geocentric latitude of position (radians)
     * @return the J70 Seasonal-Latitudinal Variation
     */
    private static double dlrsl(final double altKm, final double dateMJD, final double satLat) {
        final double capPhi = ((dateMJD - 36204.0) / 365.2422) % 1;
        final int signum = (satLat >= 0) ? 1 : -1;
        final double sinLat = FastMath.sin(satLat);
        final double hm90  = altKm - 90.;
        return 0.02 * hm90 * FastMath.exp(-0.045 * hm90) * signum * FastMath.sin(MathUtils.TWO_PI * capPhi + 1.72) * sinLat * sinLat;
    }

    /** Evaluates the J70 Seasonal-Latitudinal Variation, Equation (24).
     * @param altKm satellite altitude (km)
     * @param dateMJD Modified Julian date
     * @param satLat Geocentric latitude of position (radians)
     * @param <T> type of the elements
     * @return the J70 Seasonal-Latitudinal Variation
     */
    private static <T extends CalculusFieldElement<T>> T dlrsl(final T altKm, final T dateMJD, final T satLat) {
        T capPhi = dateMJD.subtract(36204.0).divide(365.2422);
        capPhi = capPhi.subtract(FastMath.floor(capPhi.getReal()));
        final int signum = (satLat.getReal() >= 0.) ? 1 : -1;
        final T sinLat = satLat.sin();
        final T hm90  = altKm.subtract(90.);
        return hm90.multiply(0.02).multiply(hm90.multiply(-0.045).exp()).multiply(capPhi.multiply(MathUtils.TWO_PI).add(1.72).sin()).multiply(signum).multiply(sinLat).multiply(sinLat);
    }

    /** Evaluates the high altitude exospheric density correction factor.
     * @param altKm satellite altitude in km
     * @param f10B 10.7-cm Solar Flux, averaged 81-day centered on the input time
     * @return the high altitude exospheric density correction factor
     */
    private static double densityCorrectionFactor(final double altKm, final double f10B) {
        if (altKm >= 1000.0 && altKm < 1500.0) {
            final double zeta = (altKm - 1000.) * 0.002;
            final double f15c = CHT[0] + CHT[1] * f10B + CHT[2] * 1500.0 + CHT[3] * f10B * 1500.0;
            final double f15cZeta = (CHT[2] + CHT[3] * f10B) * 500.0;
            final double fex2 = 3.0 * f15c - f15cZeta - 3.0;
            final double fex3 = f15cZeta - 2.0 * f15c + 2.0;
            return 1.0 + zeta * zeta * (fex2 + fex3 * zeta);
        }
        if (altKm >= 1500.0) {
            return CHT[0] + CHT[1] * f10B + CHT[2] * altKm + CHT[3] * f10B * altKm;
        }
        return 1.0;
    }

    /** Evaluates the high altitude exospheric density correction factor.
     * @param altKm satellite altitude in km
     * @param f10B 10.7-cm Solar Flux, averaged 81-day centered on the input time
     * @param field field of the elements
     * @param <T> type of the field elements
     * @return the high altitude exospheric density correction factor
     */
    private static <T extends CalculusFieldElement<T>> T densityCorrectionFactor(final T altKm, final double f10B,
                                                                                final Field<T> field) {
        if (altKm.getReal() >= 1000.0 && altKm.getReal() < 1500.0) {
            final T zeta = altKm.subtract(1000.).multiply(0.002);
            final double f15c = CHT[0] + CHT[1] * f10B + CHT[2] * 1500.0 + CHT[3] * f10B * 1500.0;
            final double f15cZeta = (CHT[2] + CHT[3] * f10B) * 500.0;
            final double fex2 = 3.0 * f15c - f15cZeta - 3.0;
            final double fex3 = f15cZeta - 2.0 * f15c + 2.0;
            return field.getOne().add(zeta.multiply(zeta).multiply(zeta.multiply(fex3).add(fex2)));
        }
        if (altKm.getReal() >= 1500.0) {
            return altKm.multiply(CHT[3] * f10B).add(altKm.multiply(CHT[2])).add(CHT[0] + CHT[1] * f10B);
        }
        return field.getOne();
    }

    /** Compute daily temperature correction for Jacchia-Bowman model.
     * @param f10 solar flux index
     * @param solarTime local solar time (hours in [0, 24[)
     * @param satLat sat lat (radians)
     * @param satAlt height (km)
     * @return dTc correction
     */
    private static double dTc(final double f10, final double solarTime,
                              final double satLat, final double satAlt) {
        final double st = solarTime / 24.0;
        final double cs = FastMath.cos(satLat);
        final double fs = (f10 - 100.0) / 100.0;

        // Calculates dTc according to height
        if (satAlt >= 120 && satAlt <= 200) {
            final double dtc200   = poly2CDTC(fs, st, cs);
            final double dtc200dz = poly1CDTC(fs, st, cs);
            final double cc       = 3.0 * dtc200 - dtc200dz;
            final double dd       = dtc200 - cc;
            final double zp       = (satAlt - 120.0) / 80.0;
            return zp * zp * (cc + dd * zp);

        } else if (satAlt > 200.0 && satAlt <= 240.0) {
            final double h = (satAlt - 200.0) / 50.0;
            return poly1CDTC(fs, st, cs) * h + poly2CDTC(fs, st, cs);

        } else if (satAlt > 240.0 && satAlt <= 300.0) {
            final double h        = 0.8;
            final double bb       = poly1CDTC(fs, st, cs);
            final double aa       = bb * h + poly2CDTC(fs, st, cs);
            final double p2BDT    = poly2BDTC(st);
            final double dtc300   = poly1BDTC(fs, st, cs, 3 * p2BDT);
            final double dtc300dz = cs * p2BDT;
            final double cc       = 3.0 * dtc300 - dtc300dz - 3.0 * aa - 2.0 * bb;
            final double dd       = dtc300 - aa - bb - cc;
            final double zp       = (satAlt - 240.0) / 60.0;
            return aa + zp * (bb + zp * (cc + zp * dd));

        } else if (satAlt > 300.0 && satAlt <= 600.0) {
            final double h = satAlt / 100.0;
            return poly1BDTC(fs, st, cs, h * poly2BDTC(st));

        } else if (satAlt > 600.0 && satAlt <= 800.0) {
            final double poly2 = poly2BDTC(st);
            final double aa    = poly1BDTC(fs, st, cs, 6 * poly2);
            final double bb    = cs * poly2;
            final double cc    = -(3.0 * aa + 4.0 * bb) / 4.0;
            final double dd    = (aa + bb) / 4.0;
            final double zp    = (satAlt - 600.0) / 100.0;
            return aa + zp * (bb + zp * (cc + zp * dd));

        }
        return 0.;
    }

    /** Compute daily temperature correction for Jacchia-Bowman model.
     * @param f10 solar flux index
     * @param solarTime local solar time (hours in [0, 24[)
     * @param satLat sat lat (radians)
     * @param satAlt height (km)
     * @param <T> type of the filed elements
     * @return dTc correction
     */
    private static <T extends CalculusFieldElement<T>> T dTc(final double f10, final T solarTime,
                                                             final T satLat, final T satAlt) {
        final T      st = solarTime.divide(24.0);
        final T      cs = satLat.cos();
        final double fs = (f10 - 100.0) / 100.0;

        // Calculates dTc according to height
        if (satAlt.getReal() >= 120 && satAlt.getReal() <= 200) {
            final T dtc200   = poly2CDTC(fs, st, cs);
            final T dtc200dz = poly1CDTC(fs, st, cs);
            final T cc       = dtc200.multiply(3).subtract(dtc200dz);
            final T dd       = dtc200.subtract(cc);
            final T zp       = satAlt.subtract(120.0).divide(80.0);
            return zp.multiply(zp).multiply(cc.add(dd.multiply(zp)));

        } else if (satAlt.getReal() > 200.0 && satAlt.getReal() <= 240.0) {
            final T h = satAlt.subtract(200.0).divide(50.0);
            return poly1CDTC(fs, st, cs).multiply(h).add(poly2CDTC(fs, st, cs));

        } else if (satAlt.getReal() > 240.0 && satAlt.getReal() <= 300.0) {
            final T h        = solarTime.getField().getZero().newInstance(0.8);
            final T bb       = poly1CDTC(fs, st, cs);
            final T aa       = bb.multiply(h).add(poly2CDTC(fs, st, cs));
            final T p2BDT    = poly2BDTC(st);
            final T dtc300   = poly1BDTC(fs, st, cs, p2BDT.multiply(3));
            final T dtc300dz = cs.multiply(p2BDT);
            final T cc       = dtc300.multiply(3).subtract(dtc300dz).subtract(aa.multiply(3)).subtract(bb.multiply(2));
            final T dd       = dtc300.subtract(aa).subtract(bb).subtract(cc);
            final T zp       = satAlt.subtract(240.0).divide(60.0);
            return aa.add(zp.multiply(bb.add(zp.multiply(cc.add(zp.multiply(dd))))));

        } else if (satAlt.getReal() > 300.0 && satAlt.getReal() <= 600.0) {
            final T h = satAlt.divide(100.0);
            return poly1BDTC(fs, st, cs, h.multiply(poly2BDTC(st)));

        } else if (satAlt.getReal() > 600.0 && satAlt.getReal() <= 800.0) {
            final T poly2 = poly2BDTC(st);
            final T aa    = poly1BDTC(fs, st, cs, poly2.multiply(6));
            final T bb    = cs.multiply(poly2);
            final T cc    = aa.multiply(3).add(bb.multiply(4)).divide(-4.0);
            final T dd    = aa.add(bb).divide(4.0);
            final T zp    = satAlt.subtract(600.0).divide(100.0);
            return aa.add(zp.multiply(bb.add(zp.multiply(cc.add(zp.multiply(dd))))));

        }
        return satLat.getField().getZero();
    }

    /** Calculates first polynomial with CDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @return the value of the polynomial
     */
    private static double poly1CDTC(final double fs, final double st, final double cs) {
        return CDT_SUB[0] +
               fs * (CDT_SUB[1] + st * (CDT_SUB[2] + st * (CDT_SUB[3] + st * (CDT_SUB[4] + st * (CDT_SUB[5] + st * CDT_SUB[6]))))) +
               cs * st * (CDT_SUB[7] + st * (CDT_SUB[8] + st * (CDT_SUB[9] + st * (CDT_SUB[10] + st * CDT_SUB[11])))) +
               cs * (CDT_SUB[12] + fs * (CDT_SUB[13] + st * (CDT_SUB[14] + st * CDT_SUB[15])));
    }

    /** Calculates first polynomial with CDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @param <T> type of the field elements
     * @return the value of the polynomial
     */
    private static <T extends CalculusFieldElement<T>>  T poly1CDTC(final double fs, final T st, final T cs) {
        return st.multiply(CDT_SUB[6]).
               add(CDT_SUB[5]).multiply(st).
               add(CDT_SUB[4]).multiply(st).
               add(CDT_SUB[3]).multiply(st).
               add(CDT_SUB[2]).multiply(st).
               add(CDT_SUB[1]).multiply(fs).
               add(st.multiply(CDT_SUB[11]).
               add(CDT_SUB[10]).multiply(st).
               add(CDT_SUB[ 9]).multiply(st).
               add(CDT_SUB[ 8]).multiply(st).
               add(CDT_SUB[7]).multiply(st).multiply(cs)).
               add(st.multiply(CDT_SUB[15]).
               add(CDT_SUB[14]).multiply(st).
               add(CDT_SUB[13]).multiply(fs).
               add(CDT_SUB[12]).multiply(cs)).
               add(CDT_SUB[0]);
    }

    /** Calculates second polynomial with CDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @return the value of the polynomial
     */
    private static double poly2CDTC(final double fs, final double st, final double cs) {
        return CDT_SUB[16] + st * cs * (CDT_SUB[17] + st * (CDT_SUB[18] + st * CDT_SUB[19])) +
               fs * cs * (CDT_SUB[20] + st * (CDT_SUB[21] + st * CDT_SUB[22]));
    }

    /** Calculates second polynomial with CDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @param <T> type of the field elements
     * @return the value of the polynomial
     */
    private static <T extends CalculusFieldElement<T>>  T poly2CDTC(final double fs, final T st, final T cs) {
        return st.multiply(CDT_SUB[19]).
               add(CDT_SUB[18]).multiply(st).
               add(CDT_SUB[17]).multiply(cs).multiply(st).
               add(st.multiply(CDT_SUB[22]).
               add(CDT_SUB[21]).multiply(st).
               add(CDT_SUB[20]).multiply(cs).multiply(fs)).
               add(CDT_SUB[16]);
    }

    /** Calculates first polynomial with BDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @param hp scaled height * poly2BDTC
     * @return the value of the polynomial
     */
    private static double poly1BDTC(final double fs, final double st, final double cs, final double hp) {
        return BDT_SUB[0] +
               fs * (BDT_SUB[1] + st * (BDT_SUB[2] + st * (BDT_SUB[3] + st * (BDT_SUB[4] + st * (BDT_SUB[5] + st * BDT_SUB[6]))))) +
               cs * (st * (BDT_SUB[7] + st * (BDT_SUB[8] + st * (BDT_SUB[9] + st * (BDT_SUB[10] + st * BDT_SUB[11])))) + hp + BDT_SUB[18]);
    }

    /** Calculates first polynomial with BDTC array.
     * @param fs scaled flux f10
     * @param st local solar time in [0, 1[
     * @param cs cosine of satLat
     * @param hp scaled height * poly2BDTC
     * @param <T> type of the field elements
     * @return the value of the polynomial
     */
    private static <T extends CalculusFieldElement<T>>  T poly1BDTC(final double fs, final T st, final T cs, final T hp) {
        return st.multiply(BDT_SUB[6]).
               add(BDT_SUB[5]).multiply(st).
               add(BDT_SUB[4]).multiply(st).
               add(BDT_SUB[3]).multiply(st).
               add(BDT_SUB[2]).multiply(st).
               add(BDT_SUB[1]).multiply(fs).
               add(st.multiply(BDT_SUB[11]).
               add(BDT_SUB[10]).multiply(st).
               add(BDT_SUB[ 9]).multiply(st).
               add(BDT_SUB[ 8]).multiply(st).
               add(BDT_SUB[ 7]).multiply(st).
               add(hp).add(BDT_SUB[18]).multiply(cs)).
               add(BDT_SUB[0]);
    }

    /** Calculates second polynomial with BDTC array.
     * @param st local solar time in [0, 1[
     * @return the value of the polynomial
     */
    private static double poly2BDTC(final double st) {
        return BDT_SUB[12] + st * (BDT_SUB[13] + st * (BDT_SUB[14] + st * (BDT_SUB[15] + st * (BDT_SUB[16] + st * BDT_SUB[17]))));
    }

        /** Calculates second polynomial with BDTC array.
     * @param st local solar time in [0, 1[
     * @param <T> type of the field elements
     * @return the value of the polynomial
     */
    private static <T extends CalculusFieldElement<T>>  T poly2BDTC(final T st) {
        return st.multiply(BDT_SUB[17]).
               add(BDT_SUB[16]).multiply(st).
               add(BDT_SUB[15]).multiply(st).
               add(BDT_SUB[14]).multiply(st).
               add(BDT_SUB[13]).multiply(st).
               add(BDT_SUB[12]);
    }

}