This is a Cython implementation of the NREL Solar Position Algorithm for Solar Radiation Applications. Designed for calculating solar zenith and azimuth across a temporal and spatial dimension.
git clone ...
python3 -m venv .venv && source .venv/bin/activate
pip install .
import numpy as np
import fast_spa
# 200x200km area
lats = np.linspace(30, 31, 100)
lons = np.linspace(-80, -79, 100)
lats, lons = np.meshgrid(lats, lons)
datetime_obj = (
np.arange("2023-01-01", "2023-01-02", dtype="datetime64[h]")
.astype("datetime64[ns]")
.astype(str)
.tolist()
)
%timeit fast_spa.fast_spa(datetime_obj, lats, lons)
29.1 ms ± 299 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
There is also integration with the ETOPO2022 dataset for elevation data.
import numpy as np
from numpy.typing import ArrayLike
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import fast_spa
ZENITH_ANGLE = 0
AZIMUTH_ANGLE = 1
def contourf(
ax: GeoAxes,
data: ArrayLike,
lons: ArrayLike,
lats: ArrayLike,
title="",
**kwargs,
) -> GeoAxes:
ax.title.set_text(title)
ax.coastlines()
ax.add_feature(cfeature.STATES)
ax.contourf(
lons,
lats,
data,
transform=ccrs.PlateCarree(),
**kwargs,
)
return ax
def main(
etopo: fast_spa.ETOPO2022 = fast_spa.ETOPO2022(),
lons=np.linspace(-125, -65, 200),
lats=np.linspace(25, 50, 100),
weight: float = 1e7,
):
fig, axes = plt.subplots(
2, 3, figsize=(20, 6), subplot_kw={"projection": ccrs.PlateCarree()}
)
# - plot the non-resampled elevation data
lons, lats = np.meshgrid(lons, lats)
y_mask = np.logical_and(etopo.lat >= lats.min(), etopo.lat <= lats.max())
x_mask = np.logical_and(etopo.lon >= lons.min(), etopo.lon <= lons.max())
fig.tight_layout()
e = etopo[y_mask, x_mask]
e[e < -100] = -100 # set the ocean to -100m
contourf(
axes[0, 0],
e,
etopo.lon[x_mask],
etopo.lat[y_mask],
"elevation",
cmap="terrain",
)
# - resample the elevation data to fit the lon, lat grid
e = etopo.resample(lons, lats)
e[e < -100] = -100 # set the ocean to -100m
spa_data = fast_spa.fast_spa(datetime_obj, lats, lons, e)
# - apply a weight to the elevation data for SPA calculation
weighted = fast_spa.fast_spa(datetime_obj, lats, lons, e * weight)
weighted = np.radians(weighted)
contourf(
axes[1, 0],
e,
lons,
lats,
"resampled elevation",
cmap="terrain",
)
contourf(
axes[0, 1],
spa_data[ZENITH_ANGLE, 0, :, :],
lons,
lats,
"zenith angle",
)
contourf(
axes[1, 1],
weighted[ZENITH_ANGLE, 0, :, :],
lons,
lats,
f"weighted zenith angle {weight:.0e}",
)
contourf(
axes[0, 2],
spa_data[AZIMUTH_ANGLE, 0, :, :],
lons,
lats,
"azimuth angle",
)
contourf(
axes[1, 2],
weighted[AZIMUTH_ANGLE, 0, :, :],
lons,
lats,
f"weighted azimuth angle {weight:.0e}",
)
plt.show()
main()