Skip to content

header header

Solar Position For Solar Resource Assessment

Python versions Tests Coverage DOI

sunwhere is a Python library designed for fast and accurate calculations of solar position for solar resource applications 🌞.

License

sunwhere is free for research and non-commercial use (CC BY-NC-SA 4.0). Commercial use is not permitted under this license. See License.

sunwhere provides solar zenith and azimuth angles, sun-earth distance correction factor, and secondary parameters such as solar declination, equation of time, and many more. It's optimized for typical workflows in solar energy research and engineering.

Key Features

Multiple solar position algorithms

Choose the solar position algorithm (SPA) that best fits your accuracy and performance needs:

  • NREL's SPA: Valid for years -2000 to 6000, is the most accurate (±0.0003°) but the slowest too. It is probably adequate for single-location (or few) time series but not for time series of regular grids of sites.
  • PSA's SPA (Plataforma Solar de Almería): Now with updated coefficients tuned for 2020 to 2050, is a blazing fast default in sunwhere.
  • Iqbal's SPA: The elderly guy, good for educational/low-accuracy applications

Optimized for common use cases

Three specialized functions cover most practical scenarios:

  1. sites() - Multiple arbitrary locations with common time grid
  2. regular_grid() - Lat-lon regular grids (ideal for spatio-temporal data)
  3. transect() - Moving observers (satellites, aircraft, ships)

Table 1 compares execution times of sunwhere and two popular packages for solar position calculations, pvlib and solposx, using a year of minutely data as a baseline.

Table 1. Execution time (average of 10 runs ± standard deviation) for a year of minutely data in a single location and in 10 locations randomly chosen.

Package (algorithm, engine) 1 site (ms) 10 sites (ms)
sunwhere (PSA, numexpr) 224 ± 25 602 ± 21
sunwhere (PSA, numpy) 387 ± 40 1055 ± 11
solposx (PSA, numpy) 406 ± 84 3312 ± 125
pvlib (Ephemeris, numpy) 572 ± 96 4256 ± 69
sunwhere (NREL, numexpr) 2074 ± 58 2512 ± 40
pvlib (NREL, numba) 5080 ± 156 50255 ± 783
solposx (SPA "NREL", numpy) 5114 ± 92 51348 ± 1597
sunwhere is the top performer in both cases 1 location and 10 locations
  • For one location, the differences are not significant, in practice, when using PSA but they are remarkable when using NREL (sunwhere is few seconds faster than both pvlib and solpos).
  • For 10 locations, sunwhere is significantly faster than both pvlib and solposx when using PSA (about 5-7x) and much faster when using NREL (about 20x: 2.5 s against 50 s).
  • For one location and hourly (or shorter) time series, the differences between packages are not significant and sunwhere can even be slower than the other two packages, due to its start-up overhead (validation of inputs and construction of the xarray outputs).
  • As the number of locations grows beyond 10, the time of sunwhere grows more slowly than that of the other packages. See Benchmarks for larger cases.

Modern Data Structures

Results are returned as xarray DataArrays, providing:

  • Labeled dimensions and coordinates
  • Rich metadata
  • Integration with the scientific Python ecosystem
  • Seamless saving to NetCDF, Zarr, and other formats

Quick examples

Example (sites): Solar position in multiple sites with common time grid

import matplotlib.pyplot as plt
import pandas as pd
import sunwhere

# Define locations and times
times = pd.date_range('2024-01-01', '2024-01-02', freq='1min', tz='UTC')
cities = {
    'Madrid': (40.4, -3.7),
    'Tokyo': (35.7, 139.7),
    'New York': (40.7, -74.0)
}
lats = [coord[0] for coord in cities.values()]
lons = [coord[1] for coord in cities.values()]

# Calculate solar position
result = sunwhere.sites(times, lats, lons, site_names=list(cities), algorithm='nrel')

# solar zenith angle in Madrid
print(result.zenith.sel(site='Madrid'))

# or selecting by index
print(result.zenith.isel(site=0))

result.elevation.plot.line(x='time', hue='site')
plt.show()
Check output

Example sunwhere.site

Example (regular_grid): Solar position on a regular grid of locations

import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs

import sunwhere

# select the spatial grid...
lats = np.arange(-89.5, 90, 1)
lons = np.arange(-179.5, 180, 1)

# ...and the time grid
times = np.arange('2023-01-15', '2023-01-16', np.timedelta64(6, 'h'), dtype='datetime64[ns]')
# or: times = pd.date_range('2023-01-15', '2023-01-16', freq='6h', inclusive='left')

result = sunwhere.regular_grid(times, lats, lons)  # algorithm=psa by default

# draw some results...
plt.rcParams['axes.titlesize'] = 'small'
plt.rcParams['axes.labelsize'] = 'small'
plt.rcParams['xtick.labelsize'] = 'small'
plt.rcParams['ytick.labelsize'] = 'small'
plt.figure(figsize=(14, 8), layout='constrained')
for k in range(len(result.sza.time)):
    ax = plt.subplot(4, 3, 3*k+1, projection=ccrs.PlateCarree())
    result.sza.isel(time=k).plot(ax=ax, cmap='inferno_r')
    ax.coastlines(lw=0.3, color='w')
    ax = plt.subplot(4, 3, 3*k+2, projection=ccrs.PlateCarree())
    result.cosz.isel(time=k).plot(ax=ax, cmap='RdBu_r')
    ax.coastlines(lw=0.5, color='0.3')
    ax = plt.subplot(4, 3, 3*k+3, projection=ccrs.PlateCarree())
    result.incidence(30, 60).isel(time=k).plot(ax=ax, cmap='RdBu_r')
    ax.coastlines(lw=0.5, color='0.3')

plt.show()
Check output

Example sunwhere.regular_grid

Example (transect): Solar position through a transect

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import cartopy.crs as ccrs

import sunwhere

def build_transect_path(time0, lat0, lon0, t_sim, dt, vx, vy):
    # vx and vy are velocities in degrees/hour
    time1 = time0 + pd.Timedelta(t_sim)
    times = pd.date_range(time0, time1, freq=pd.Timedelta(dt), tz='UTC')
    dlat = vy * (times.freq / pd.Timedelta(1, 'h'))
    lats = lat0 + dlat*np.arange(len(times))
    dlon = vx * (times.freq / pd.Timedelta(1, 'h'))
    lons = lon0 + dlon*np.arange(len(times))
    lons[lons < -180] = lons[lons < -180] % 180
    lons[lons >= 180] = lons[lons >= 180] % -180
    return times, lats, lons

def draw_transect(sunpos, axpos):
    azimuth = sunpos.azimuth
    times = azimuth.coords['time']
    lats = azimuth.coords['lat']
    lons = azimuth.coords['lon']
    elevation = sunpos.elevation

    ax = plt.subplot(axpos[0], projection=ccrs.Robinson())
    kwargs = {"transform": ccrs.PlateCarree(), "c": np.array(times, dtype=float)}
    ax.scatter(lons, lats, marker='.', s=2, **kwargs)
    ax.coastlines(lw=0.5, color='0.5')
    ax.set_global()

    elevation.plot(ax=plt.subplot(axpos[1]))
    plt.ylim(-90, 90)

    azimuth.plot(ax=plt.subplot(axpos[2]))
    plt.ylim(-180, 180)

# starting time and location
time0 = pd.Timestamp('2023-07-15 12')
lat0, lon0 = -40., -120.
ers = 360/24  # earth rotation speed, in degrees/hour

# draw some results...
plt.rcParams['axes.titlesize'] = 'small'
plt.rcParams['axes.labelsize'] = 'small'
plt.rcParams['xtick.labelsize'] = 'small'
plt.rcParams['ytick.labelsize'] = 'small'
plt.figure(figsize=(12, 6), layout='constrained')

times, lats, lons = build_transect_path(time0, 40, 0, '23h', '10s', -ers, 0.)
result = sunwhere.transect(times, lats, lons)  # algorithm='psa' refraction=True
draw_transect(result, (331, 334, 337))
plt.subplot(331).set_title('Westward transect')

times, lats, lons = build_transect_path(time0, -40, 0, '23h', '10s', ers, 0.)
result = sunwhere.transect(times, lats, lons)  # algorithm='psa' refraction=True
draw_transect(result, (332, 335, 338))
plt.subplot(332).set_title('Eastward transect')

times, lats, lons = build_transect_path(time0, -40, -120, '10h', '10s', 1.2*ers, 0.8*ers)
result = sunwhere.transect(times, lats, lons)  # algorithm='psa' refraction=True
draw_transect(result, (333, 336, 339))
plt.subplot(333).set_title('SW-NE transect')

plt.show()
Check output

Example sunwhere.transect

Getting Started

Ready to use sunwhere? Check out the Installation guide and the User Guide to get started! The accuracy and speed of the algorithms are compared with other packages in Benchmarks.

References

  • NREL SPA: Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar Radiation Applications. NREL Report No. TP-560-34302. PDF

  • PSA Algorithm: Blanco, M. et al., 2020. Updating the PSA sun position algorithm. Solar Energy, 212, 339-341. DOI

  • Iqbal Algorithm: Iqbal, M., 1983. An introduction to solar radiation. Academic Press.

License

sunwhere is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license (CC BY-NC-SA 4.0):

  • Free for research, teaching and other non-commercial use, with attribution.
  • Commercial use is not permitted under this license. For commercial use, contact the author.
  • Derivative works must be distributed under the same license.

Citation

If you use sunwhere in your research, please cite:

@software{sunwhere,
  author = {Ruiz-Arias, Jose A.},
  title = {sunwhere: Solar position for solar resource assessment},
  doi = {10.5281/zenodo.21975084},
  url = {https://github.com/jararias/sunwhere}
}

The DOI 10.5281/zenodo.21975084 always resolves to the latest version. Each release also has its own version-specific DOI, listed on Zenodo.