nrel-spa
A faithful TypeScript port of the NREL Solar Position Algorithm by Ibrahim Reda and Afshin Andreas (2008). Achieves ±0.0003° over the years −2000 to +6000.
Overview
nrel-spa is the solar position engine used internally by pray-calc. You can use it directly for any application that needs precise solar coordinates.
- Based on NREL/TP-560-34302 (Reda & Andreas, 2008)
- ±0.0003° accuracy
- Outputs topocentric altitude, azimuth, right ascension, declination, hour angle
- Full atmospheric refraction model (pressure + temperature)
- TypeScript-first, ESM + CJS
GitHub: github.com/acamarata/nrel-spa
Installation
<span><span style="color: var(--shiki-token-function)">pnpm</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">add</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">nrel-spa</span></span>
<span></span>
computeSolarPosition
<span><span style="color: var(--shiki-token-function)">computeSolarPosition</span><span style="color: var(--shiki-color-text)">(options: SolarPositionOptions): SolarPosition</span></span>
<span></span>
Options
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
date | Date | Yes | — | Date and time (UTC) |
latitude | number | Yes | — | Decimal degrees |
longitude | number | Yes | — | Decimal degrees |
elevation | number | No | 0 | Metres above sea level |
pressure | number | No | 1013.25 | Millibars |
temperature | number | No | 15 | Celsius |
deltaT | number | No | 69.18 | ΔT in seconds |
slope | number | No | 0 | Surface tilt angle (0 = horizontal) |
azimuthRotation | number | No | 0 | Surface azimuth rotation |
atmosphericRefraction | number | No | 0.5667 | Horizon refraction in degrees |
Returns
| Field | Unit | Description |
|---|---|---|
topocentricAltitude | degrees | Sun's altitude above horizon (corrected for refraction) |
topocentricAzimuth | degrees | Sun's azimuth clockwise from north |
topocentricRightAscension | hours | Equatorial RA (topocentric) |
topocentricDeclination | degrees | Equatorial declination (topocentric) |
localHourAngle | degrees | Hour angle from meridian |
zenithAngle | degrees | Angle from zenith (90° − altitude) |
geocentricSunDistance | AU | Earth-Sun distance in astronomical units |
equationOfTime | minutes | ΔT between solar noon and 12:00 local mean time |
<span><span style="color: var(--shiki-token-keyword)">import</span><span style="color: var(--shiki-color-text)"> { computeSolarPosition } </span><span style="color: var(--shiki-token-keyword)">from</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'nrel-spa'</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">pos</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">computeSolarPosition</span><span style="color: var(--shiki-color-text)">({</span></span>
<span><span style="color: var(--shiki-color-text)"> date</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Date</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'2025-06-21T09:00:00Z'</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)"> latitude</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">21.4225</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)"> longitude</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">39.8262</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// Mecca</span></span>
<span><span style="color: var(--shiki-color-text)"> elevation</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">270</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)"> pressure</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">982</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// adjusted for 270m elevation</span></span>
<span><span style="color: var(--shiki-color-text)"> temperature</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">38</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// summer temperature in Mecca</span></span>
<span><span style="color: var(--shiki-color-text)">})</span></span>
<span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">`Altitude: </span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-token-constant)">pos</span><span style="color: var(--shiki-token-function)">.</span><span style="color: var(--shiki-token-constant)">topocentricAltitude</span><span style="color: var(--shiki-token-function)">.toFixed</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">3</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">°`</span><span style="color: var(--shiki-color-text)">)</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">`Azimuth: </span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-token-constant)">pos</span><span style="color: var(--shiki-token-function)">.</span><span style="color: var(--shiki-token-constant)">topocentricAzimuth</span><span style="color: var(--shiki-token-function)">.toFixed</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">3</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">°`</span><span style="color: var(--shiki-color-text)">)</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">`Equation of time: </span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-token-constant)">pos</span><span style="color: var(--shiki-token-function)">.</span><span style="color: var(--shiki-token-constant)">equationOfTime</span><span style="color: var(--shiki-token-function)">.toFixed</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">2</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)"> min`</span><span style="color: var(--shiki-color-text)">)</span></span>
<span></span>
Accuracy
The algorithm achieves ±0.0003° (about 1 arcsecond) for the years −2000 to +6000. For comparison:
| Algorithm | Accuracy | Source |
|---|---|---|
| NREL SPA | ±0.0003° | Reda & Andreas (2008) |
| Jean Meeus (simplified) | ±0.01° | Astronomical Algorithms (1991) |
| VSOP87 (full) | ±0.0001° | Bretagnon & Francou (1988) |
| SunCalc.js | ~±0.1° | Web-optimized approximation |
For prayer times, ±0.0003° corresponds to a timing error of less than 1 second. The simplified Meeus method produces errors up to 30 seconds at equinoxes and near solstices at high latitudes.
Algorithm reference
The NREL SPA implements the following computational chain:
- Julian Day and Julian Ephemeris Day
- Heliocentric longitude and latitude (Fourier series, 2,500+ terms from VSOP87D)
- Geocentric longitude and latitude
- Nutation in longitude (Δψ) and obliquity (Δε)
- Aberration correction
- Apparent Sun longitude and right ascension
- Observer local hour angle
- Topocentric coordinates (parallax correction)
- Atmospheric refraction correction
Reference: Reda, I.; Andreas, A. (2008). Solar Position Algorithm for Solar Radiation Applications. NREL/TP-560-34302. Golden, CO: National Renewable Energy Laboratory.