Class SPACalculator

All Implemented Interfaces:
Cloneable

Implementation of solar times using the Solar Position Algorithm (SPA). This was the work of Ibrahim Reda and Afshin Andreas (NREL/TP-560-34302, Solar Energy 76 (2004) 577-589). SPA computes the Earth's heliocentric position from the VSOP87 series, applies nutation, aberration and the obliquity of the ecliptic to obtain the Sun's apparent geocentric right ascension and declination, then performs the full topocentric reduction - observer parallax (using latitude, longitude and elevation) and atmospheric refraction - to yield the observed topocentric zenith and azimuth. Its stated uncertainty is ±0.0003° (about one arc-second) for the years −2000 to +6000.

Refraction is fully configurable through the parent AstronomicalCalculator. Many SPA ports hardcode their refraction model (and even their solar radius); this one does not:

ΔT (TT − UT) is integral to SPA and is applied here. It defaults to an Espenak-Meeus estimate but can be overridden exactly via setDeltaTOverride(Double) (the published SPA test case fixes ΔT = 67 seconds), or disabled via setApplyDeltaT(boolean).

Topocentric note. Unlike NOAACalculator.getSolarElevation(Instant, GeoLocation) (sea-level, altitude-independent), this calculator's elevation / azimuth are genuinely topocentric: the observer's elevation feeds the parallax term. The adjustForElevation flag on sunrise / sunset continues to control only the geometric horizon dip via the parent.

Coefficient coverage. The VSOP87 Earth tables are the abridged Appendix III set (validated to about 0.1′ against Meeus's worked example), and the nutation uses the principal terms. These give a few arc-seconds of accuracy - vastly beyond any zmanim requirements. For the full published SPA ±0.0003° envelope, the VSOP87 tables and the IAU 1980 nutation series can be extended without changing the algorithm.

Author:
© Eliyahu Hershfeld 2026; NREL SPA (Reda and Andreas) port
  • Field Details

    • JULIAN_DAY_JAN_1_2000

      private static final double JULIAN_DAY_JAN_1_2000
      The Julian day of J2000.0.
      See Also:
    • JULIAN_DAYS_PER_CENTURY

      private static final double JULIAN_DAYS_PER_CENTURY
      Julian days per century.
      See Also:
    • applyDeltaT

      private boolean applyDeltaT
      Whether to apply the ΔT (TT−UT) correction. Defaults to true.
    • deltaTOverride

      An explicit ΔT (seconds) override; null means estimate it from the date.
    • pressure

      private double pressure
      Annual average local pressure in millibars, used by the SPA refraction model. Default 1013.25.
    • temperature

      private double temperature
      Annual average local temperature in °C, used by the SPA refraction model. Default 10.
    • EARTH_L

      private static final double[][][] EARTH_L
      Earth heliocentric longitude series L0..L5.
    • EARTH_B

      private static final double[][][] EARTH_B
      Earth heliocentric latitude series B0..B1.
    • EARTH_R

      private static final double[][][] EARTH_R
      Earth radius-vector series R0..R4 (AU).
  • Constructor Details

    • SPACalculator

      public SPACalculator()
      Default constructor of the SPACalculator.
  • Method Details

    • getCalculatorName

      Description copied from class: AstronomicalCalculator
      Returns the name of the algorithm.
      Specified by:
      getCalculatorName in class AstronomicalCalculator
      Returns:
      the descriptive name of the algorithm.
    • setApplyDeltaT

      public void setApplyDeltaT(boolean applyDeltaT)
      Sets to apply ΔT true (default), or false to omit it (UT = TT).
      Parameters:
      applyDeltaT - true (default) to apply ΔT, false to omit it (UT = TT).
      See Also:
    • isApplyDeltaT

      public boolean isApplyDeltaT()
      Returns whether the ΔT correction is being applied. @see #setApplyDeltaT(boolean).
      Returns:
      whether the ΔT correction is being applied. @see #setApplyDeltaT(boolean)
    • setDeltaTOverride

      public void setDeltaTOverride(Double deltaTSeconds)
      Override the estimated ΔT with an explicit value in seconds (for example 67 seconds for the published SPA test case), or null to revert to the date-based estimate.
      Parameters:
      deltaTSeconds - ΔT in seconds, or null to estimate.
    • getDeltaTOverride

      Return the explicit ΔT (TT - UT) override in seconds, or null if ΔT is being estimated from the date (the default). Note that this returns the override setting, not the estimated value in use.
      Returns:
      the explicit ΔT override in seconds, or null if it is being estimated from the date.
      See Also:
    • setPressure

      public void setPressure(double pressureMillibars)
      Set the annual average local pressure used by the SPA atmospheric-refraction model. This affects only getSolarElevation(Instant, GeoLocation) and getSolarAzimuth(Instant, GeoLocation); it has no effect on sunrise, sunset, twilight or any depression-angle zman, whose refraction (if any) comes from the parent's fixed AstronomicalCalculator.getRefraction(). See the class documentation.
      Parameters:
      pressureMillibars - the pressure in millibars (hPa).
    • getPressure

      public double getPressure()
      Return the pressure in millibars used by the refraction model.
      Returns:
      the pressure in millibars used by the refraction model.
    • setTemperature

      public void setTemperature(double temperatureCelsius)
      Set the annual average local temperature used by the SPA atmospheric-refraction model. This affects only getSolarElevation(Instant, GeoLocation) and getSolarAzimuth(Instant, GeoLocation); it has no effect on sunrise, sunset, twilight or any depression-angle zman, whose refraction (if any) comes from the parent's fixed AstronomicalCalculator.getRefraction(). See the class documentation.
      Parameters:
      temperatureCelsius - the temperature in degrees Celsius.
    • getTemperature

      public double getTemperature()
      Returns the temperature in °C used by the refraction model.
      Returns:
      the temperature in °C used by the refraction model.
    • getUTCSunrise

      public double getUTCSunrise(LocalDate dt, GeoLocation geoLocation, double zenith, boolean adjustForElevation)
      Description copied from class: AstronomicalCalculator
      A method that calculates UTC sunrise as well as any time based on an angle above or below sunrise. This abstract method is implemented by the classes that extend this class.
      Specified by:
      getUTCSunrise in class AstronomicalCalculator
      Parameters:
      dt - The LocalDate representing the date to calculate sunrise for.
      geoLocation - The location information used for astronomical calculation of solar times.
      zenith - the azimuth below the vertical zenith of 90°. For sunrise typically the zenith used for the calculation uses geometric zenith of 90° and adjusts this slightly to account for solar refraction and the sun's radius. Another example would be AstronomicalCalendar.getBeginNauticalTwilight() that passes AstronomicalCalendar.NAUTICAL_ZENITH to this method.
      adjustForElevation - Should the time be adjusted for elevation
      Returns:
      The UTC time of sunrise in 24-hour format. 5:45:00 AM will return 5.75. If an error was encountered in the calculation (expected behavior for some locations such as near the poles, Double.NaN will be returned.
      See Also:
    • getUTCSunset

      public double getUTCSunset(LocalDate dt, GeoLocation geoLocation, double zenith, boolean adjustForElevation)
      Description copied from class: AstronomicalCalculator
      A method that calculates UTC sunset as well as any time based on an angle above or below sunset. This abstract method is implemented by the classes that extend this class.
      Specified by:
      getUTCSunset in class AstronomicalCalculator
      Parameters:
      dt - The LocalDate> representing the date to calculate sunset for.
      geoLocation - The location information used for astronomical calculation of solar times.
      zenith - the azimuth below the vertical zenith of 90°. For sunset typically the zenith used for the calculation uses geometric zenith of 90° and adjusts this slightly to account for solar refraction and the sun's radius. Another example would be AstronomicalCalendar.getEndNauticalTwilight() that passes AstronomicalCalendar.NAUTICAL_ZENITH to this method.
      adjustForElevation - Should the time be adjusted for elevation
      Returns:
      The UTC time of sunset in 24-hour format. 5:45:00 AM will return 5.75. If an error was encountered in the calculation (expected behavior for some locations such as near the poles, Double.NaN will be returned.
      See Also:
    • getUTCSunRiseSet

      private double getUTCSunRiseSet(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation, SPACalculator.SolarEvent solarEvent)
      Calculate UTC sunrise or sunset (or any twilight angle) in 24-hour format. The horizon geometry (refraction, the sun's radius and the elevation dip) is supplied entirely by the parent AstronomicalCalculator.adjustZenith(double, double, LocalDate), so the SPA topocentric solution is solved for the un-refracted, parallax-corrected center of the Sun reaching that zenith.
      Parameters:
      localDate - the date.
      geoLocation - the observer location.
      zenith - the (un-adjusted) zenith; adjusted by the parent before use.
      adjustForElevation - whether to include the geometric elevation dip.
      solarEvent - SPACalculator.SolarEvent.SUNRISE or SPACalculator.SolarEvent.SUNSET.
      Returns:
      the UTC time in hours, or Double.NaN if the event does not occur (polar day / night).
    • solveRiseSet

      private double solveRiseSet(LocalDate localDate, GeoLocation geoLocation, double adjustedZenith, SPACalculator.SolarEvent solarEvent)
      Solve for the UTC time, in minutes after 0:00, at which the topocentric (parallax-corrected, un-refracted) center of the Sun reaches adjustedZenith. A first guess is taken from the closed-form hour-angle equation (geocentric declination at local noon), then refined by a secant iteration on the full SPA topocentric zenith.
      Parameters:
      localDate - the date.
      geoLocation - the observer location.
      adjustedZenith - the target zenith (already including refraction / radius / dip from the parent).
      solarEvent - SPACalculator.SolarEvent.SUNRISE or SPACalculator.SolarEvent.SUNSET.
      Returns:
      UTC minutes after 0:00, or Double.NaN if the event does not occur.
    • getUTCNoon

      public double getUTCNoon(LocalDate localDate, GeoLocation geoLocation)
      Return the Universal Coordinated Time (UTC) of solar noon (UTC) for the given day at the given location. The NOAACalculator, the default calculator implementation calculates true solar noon, something that can be calculated even in the Arctic / Antarctic where there may be no sunrise or sunset, while the SunTimesCalculator approximates it, calculating the time as halfway between sunrise and sunset, something that can't be calculated in Polar regions where there is no sunrise or sunset for part of the year. See The Definition of Chatzos for details on solar noon / midnight calculations.
      Specified by:
      getUTCNoon in class AstronomicalCalculator
      Parameters:
      localDate - The LocalDate representing the date to calculate noon for.
      geoLocation - The location information used for astronomical calculation of solar times.
      Returns:
      The UTC time of solar noon in 24-hour format. 1:45:00 PM will return 13.75. If an error was encountered in the the calculation (expected behavior for some locations such as near the poles, Double.NaN will be returned.
      See Also:
    • getUTCMidnight

      public double getUTCMidnight(LocalDate localDate, GeoLocation geoLocation)
      Return the Universal Coordinated Time (UTC) of solar midnight (UTC) for the given day at the given location. The NOAACalculator, the default calculator implementation calculates true solar midnight, something that can be calculated even in the Arctic / Antarctic where there may be no sunrise or sunset, while the SunTimesCalculator approximates it, calculating the time as 12 hours after halfway between sunrise and sunset, something that can't be calculated in Polar regions where there is no sunrise or sunset for part of the year. See The Definition of Chatzos for details on solar noon / midnight calculations.
      Specified by:
      getUTCMidnight in class AstronomicalCalculator
      Parameters:
      localDate - The LocalDate representing the date to calculate midnight for. The calculation will be for midnight at the end of the day passed in.
      geoLocation - The location information used for astronomical calculation of solar times.
      Returns:
      The UTC time of solar midnight in a 24-hour double format. 1:45:00 AM will return 1.75. If an error was encountered in the calculation (expected behavior for the SunTimesCalculator at times of the year in Polar regions), Double.NaN will be returned.
      See Also:
    • solveNoonMidnight

      private double solveNoonMidnight(double julianDay, double lonWest, SPACalculator.SolarEvent solarEvent)
      Solar transit (noon) or anti-transit (midnight) in UTC minutes after 0:00, using the high-accuracy SPA equation of time. Parallax has a negligible (sub-second) effect on transit time, so the geocentric equation of time is used.
      Parameters:
      julianDay - the (UT) Julian day for 0:00.
      lonWest - the observer longitude in degrees, positive west.
      solarEvent - SPACalculator.SolarEvent.NOON or SPACalculator.SolarEvent.MIDNIGHT.
      Returns:
      UTC minutes after 0:00.
    • getTimeAtAzimuth

      public double getTimeAtAzimuth(LocalDate localDate, GeoLocation geoLocation, double targetAzimuth)
      Returns the time that the azimuth will occur for the date and location passed to this method. As an example, to know when the sun will be directly west, pass in an azimuth of 270°, and for directly east, pass in an azimuth of 90°. The current implementation in this class only supports azimuth values of 90° (directly east) or 270° (directly west) that are directly needed in this library for the ComprehensiveZmanimCalendar.getPolarSunsetBenIshChai() and ComprehensiveZmanimCalendar.getPolarSunriseBenIshChai().
      Specified by:
      getTimeAtAzimuth in class AstronomicalCalculator
      Parameters:
      localDate - The LocalDate representing the date to calculate the time of the azimuth for.
      geoLocation - The location information used for astronomical calculation of solar times.
      targetAzimuth - the azimuth to run the calculation for
      Returns:
      The UTC time that the azimuth will be reached in a 24-hour double format. 5:45:00 PM will return 17.75. If an error was encountered in the calculation (expected behavior for some dates at latitudes below 23.44°, a Double.NaN will be returned.
      Throws:
      IllegalArgumentException - if the azimuth is not 90° or 270°.
      TODO:
      complete the implementation for other azimuths. While not needed by this library, they may be of value to some projects. There will be edge cases where the azimuth will occur more than once a day when based on the equation of time, the day is shorter than 24 hours. In that case, the time for the first one will be returned.
    • getSolarElevation

      public double getSolarElevation(Instant instant, GeoLocation geoLocation)
      Description copied from class: AstronomicalCalculator
      Return the Solar Elevation for the horizontal coordinate system at the given location at the given time. Can be negative if the sun is below the horizon. Not corrected for altitude.
      Specified by:
      getSolarElevation in class AstronomicalCalculator
      Parameters:
      instant - The instant to calculate the elevation for.
      geoLocation - The location information used for astronomical calculation of solar times.
      Returns:
      solar elevation in degrees. The horizon (calculated in a vacuum using the solar radius as the point)is 90°, civil twilight is 96° etc. This means that sunrise and sunset that do use refraction and are calculated from the upper limb of the sun will return about 0.8333°.
    • getSolarAzimuth

      public double getSolarAzimuth(Instant instant, GeoLocation geoLocation)
      Description copied from class: AstronomicalCalculator
      Return the Solar Azimuth for the horizontal coordinate system at the given location at the given time. Not corrected for altitude. True south is 180°.
      Specified by:
      getSolarAzimuth in class AstronomicalCalculator
      Parameters:
      instant - The instant to calculate the azimuth for.
      geoLocation - The location information used for astronomical calculation of solar times.
      Returns:
      the solar azimuth in degrees. Astronomical midday would be 180° in the northern hemisphere and 0° in the southern hemisphere. Depending on the location and time of year, sunrise will have an azimuth of about 90° and sunset about 270°.
    • solarCoords

      private double[] solarCoords(double julianDayUT)
      Compute the Sun's geocentric apparent coordinates and the apparent sidereal time for the given UT Julian day.
      Parameters:
      julianDayUT - the UT Julian day (including fractional day).
      Returns:
      {alpha (geocentric apparent RA, deg 0-360), delta (geocentric apparent declination, deg), epsilon (true obliquity, deg), nu (apparent Greenwich sidereal time, deg 0-360), radius (AU), lambda (apparent longitude, deg)}.
    • topocentric

      private double[] topocentric(double julianDayUT, double latitude, double longitude, double elevationMeters)
      Full SPA topocentric reduction (observer parallax + atmospheric refraction).
      Parameters:
      julianDayUT - the UT Julian day.
      latitude - observer latitude in degrees.
      longitude - observer longitude in degrees, positive east.
      elevationMeters - observer elevation in meters.
      Returns:
      {trueElevation (topocentric, no refraction, deg), observedElevation (refraction-corrected, deg), azimuth (eastward from north, deg 0-360)}.
    • topocentricTrueZenith

      private double topocentricTrueZenith(double julianDayUT, double latitude, double longitude, double elevationMeters)
      Topocentric true (un-refracted) zenith of the Sun's center, used by the rise/set solver so that refraction is supplied solely by the parent adjustZenith.
      Parameters:
      julianDayUT - the UT Julian day.
      latitude - observer latitude in degrees.
      longitude - observer longitude in degrees, positive east.
      elevationMeters - observer elevation in meters.
      Returns:
      the topocentric true zenith in degrees.
    • refractionCorrection

      private double refractionCorrection(double trueElevation)
      The SPA atmospheric-refraction correction (Reda and Andreas) in degrees. The horizon refraction parameter is the parent's AstronomicalCalculator.getRefraction(), and the pressure / temperature are settable, so the model is fully configurable rather than hardcoded.
      Parameters:
      trueElevation - the true (un-refracted) topocentric elevation in degrees.
      Returns:
      the refraction correction in degrees to add to the true elevation.
    • equationOfTime

      private double equationOfTime(double julianDayUT)
      The equation of time (apparent minus mean solar time) in minutes of time (Meeus chapter 28), built from the SPA geocentric apparent right ascension. The sign matches NOAACalculator.
      Parameters:
      julianDayUT - the UT Julian day.
      Returns:
      the equation of time in minutes.
    • getJulianDay

      private static double getJulianDay(LocalDate localDate)
      Return the Julian day from a Java LocalDate. Julian day for 0h UT of the date (Meeus chapter 7).
      Parameters:
      localDate - the LocalDate to get the Julian date for.
      Returns:
      the Julian day corresponding to the date Note: Number is returned for the start of the Julian day. Fractional days / time should be added later.
    • julianDayFromInstant

      private static double julianDayFromInstant(Instant instant)
      Julian day (including fractional day) for an Instant in UTC.
      Parameters:
      instant - the day to calculate the Julian day for.
      Returns:
      the Julian day as a double
    • meanObliquity

      private static double meanObliquity(double jme)
      The mean obliquity of the ecliptic in degrees (SPA / Meeus 22.2 polynomial in U = JME/10).
      Parameters:
      jme - Julian millennia of ephemeris time since J2000.0.
      Returns:
      mean obliquity in degrees.
    • nutation

      private static double[] nutation(double jce)
      Principal nutation terms in longitude (Δψ) and obliquity (Δε), returned in degrees (Meeus chapter 22, abridged). These dominant terms give sub-arc-second nutation; the full IAU 1980 series can be substituted for the complete SPA precision.
      Parameters:
      jce - Julian centuries of ephemeris time since J2000.0.
      Returns:
      {Δψ, Δε} in degrees.
    • estimateDeltaT

      private static double estimateDeltaT(double julianDay)
      Estimate ΔT = TT−UT in seconds (Espenak and Meeus polynomials). Approximate but good to a few seconds for the modern era.
      Parameters:
      julianDay - the (UT) Julian day.
      Returns:
      ΔT in seconds.
    • sumSeries

      private static double sumSeries(double[][][] series, double tau)
      Evaluate a VSOP87 series Σni Ai·cos(Bi + Ci·τ)) · τn with coefficients in 1e-8 of the native unit (radians or AU).
      Parameters:
      series - indexed [n][term][{A,B,C}].
      tau - time in Julian millennia from J2000.0 (JME).
      Returns:
      the evaluated quantity (radians or AU).
    • equals

      public boolean equals(Object object)

      Two AstronomicalCalculator instances are considered equal if their Earth radius, refraction, and solar radius values are identical.

      Two SPACalculator instances are considered equal if their isApplyDeltaT(), getDeltaTOverride(), getPressure() and getTemperature() values are identical.

      Overrides:
      equals in class AstronomicalCalculator
      Parameters:
      object - the reference object with which to compare
      Returns:
    • hashCode

      public int hashCode()

      This implementation hashes the earth radius, refraction, and solar radius properties to maintain the contract with AstronomicalCalculator.equals(Object).

      This implementation hashes the applyDeltaT, deltaTOverride, pressure, and temperature properties to maintain the contract with equals(Object).

      Overrides:
      hashCode in class AstronomicalCalculator
      Returns: