
Solar Position For Solar Resource Assessment
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:
sites()- Multiple arbitrary locations with common time gridregular_grid()- Lat-lon regular grids (ideal for spatio-temporal data)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 (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 (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
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.