#!/usr/bin/env python3 """ Quick script to compare clear-sky irradiance computations from three different libraries. Among other considerations, this helped us to settle on pvlib. """ from typing import List, Dict from datetime import datetime, timedelta import solarpy import pvlib import pysolar import matplotlib.dates as mpl_dates import matplotlib.pyplot as plt import pytz from pandas import DatetimeIndex from tzwhere import tzwhere from astral import LocationInfo from astral.sun import sun DAY = datetime(2021, 2, 10, tzinfo=pytz.utc) tzwhere = tzwhere.tzwhere() locations = { "Amsterdam": (52.370216, 4.895168), "Tokyo": (35.6684415, 139.6007844), "Dallas": (32.779167, -96.808891), "Cape-Town": (-33.943707, 18.588740), # check southern hemisphere, too } datetimes = [DAY + timedelta(minutes=i * 20) for i in range(24 * 3)] timezones = {k: tzwhere.tzNameAt(*v) for k, v in locations.items()} def irradiance_by_solarpy( latitude: float, longitude: float, dt: datetime, z: str, metric: str = "dni" ) -> float: """Supports direct horizontal irradiance and direct normal irradiance.""" h = 0 # sea-level dt = dt.astimezone(pytz.timezone(z)).replace(tzinfo=None) # local time dt = solarpy.standard2solar_time(dt, longitude) # solar time if metric == "dhi": # direct horizontal irradiance vnorm = [0, 0, -1] # plane pointing up elif metric == "dni": # direct normal irradiance vnorm = solarpy.solar_vector_ned( dt, latitude ) # plane pointing directly to the sun vnorm[-1] = vnorm[-1] * 0.99999 # avoid floating point error else: return NotImplemented return solarpy.irradiance_on_plane(vnorm, h, dt, latitude) def irradiance_by_pysolar( latitude: float, longitude: float, dt: datetime, method: str = "dni" ) -> float: """Supports direct normal irradiance.""" altitude_deg = pysolar.solar.get_altitude(latitude, longitude, dt) if method == "dni": return pysolar.radiation.get_radiation_direct(dt, altitude_deg) else: return NotImplemented def irradiance_by_pvlib( latitude: float, longitude: float, dt: datetime, method: str = "dni" ) -> float: """ Supports direct horizontal irradiance, direct normal irradiance and global horizontal irradiance. https://firstgreenconsulting.wordpress.com/2012/04/26/differentiate-between-the-dni-dhi-and-ghi/ """ site = pvlib.location.Location(latitude, longitude, tz=pytz.utc) solpos = site.get_solarposition(DatetimeIndex([dt])) irradiance = site.get_clearsky(DatetimeIndex([dt]), solar_position=solpos).loc[dt] if method in ("ghi", "dni", "dhi"): return irradiance[method] else: return NotImplemented def plot_irradiance( city: str, datetimes: List[datetime], values: Dict[str, List[float]], sun_times: Dict[str, datetime], ): fig, ax = plt.subplots() ax.set( xlabel="Time (20m)", ylabel="Direct Normal Irradiance (W/m²)", title=f"Irradiance for {city} on {DAY.date()}", ) # draw values date_ticks = mpl_dates.date2num(datetimes) for lib in ("pysolar", "solarpy", "pvlib"): plt.plot_date(date_ticks, values[lib], "-", label=lib) # make date ticks look okay plt.gca().xaxis.set_major_locator(mpl_dates.HourLocator()) plt.setp(plt.gca().xaxis.get_majorticklabels(), "rotation", 40) # draw day phases boxes dawn_tick, sunrise_tick, noon_tick, sunset_tick, dusk_tick = mpl_dates.date2num( ( sun_times["dawn"], sun_times["sunrise"], sun_times["noon"], sun_times["sunset"], sun_times["dusk"], ) ) dawn_to_sunrise = plt.Rectangle( (dawn_tick, -100), sunrise_tick - dawn_tick, 1100, fc="floralwhite", ec="lemonchiffon", label="Dawn to Sunrise", ) plt.gca().add_patch(dawn_to_sunrise) sunrise_to_sunset = plt.Rectangle( (sunrise_tick, -100), sunset_tick - sunrise_tick, 1100, fc="lightyellow", ec="lemonchiffon", label="Sunrise to sunset", ) plt.gca().add_patch(sunrise_to_sunset) sunset_to_dusk = plt.Rectangle( (sunset_tick, -100), dusk_tick - sunset_tick, 1100, fc="oldlace", ec="lemonchiffon", label="Sunset to dusk", ) plt.gca().add_patch(sunset_to_dusk) # draw noon plt.axvline(x=noon_tick, color="gold", label="Noon") plt.legend() fig.savefig(f"test-irradiance-{city}.png") plt.show() if __name__ == "__main__": for city in locations: values = dict(pysolar=[], solarpy=[], pvlib=[]) lat, lon = locations[city] timezone = timezones[city] loc_info = LocationInfo(timezone=timezone, latitude=lat, longitude=lon) # this gives 'dawn', 'sunrise', 'noon', 'sunset' and 'dusk' sun_times = sun(loc_info.observer, date=DAY.date(), tzinfo=loc_info.timezone) local_datetimes = [ dt.replace(tzinfo=pytz.timezone(timezones[city])) for dt in datetimes ] for dt in local_datetimes: irrad_pysolar = irradiance_by_pysolar(lat, lon, dt) values["pysolar"].append(irrad_pysolar) irrad_solarpy = irradiance_by_solarpy(lat, lon, dt, timezone) values["solarpy"].append(irrad_solarpy) irrad_pvlib = irradiance_by_pvlib(lat, lon, dt) values["pvlib"].append(irrad_pvlib) print( f"For {city} at {dt} {timezones[city]} ― pysolar: {irrad_pysolar:.2f}, solarpy: {irrad_solarpy:.2f}, pvlib: {irrad_pvlib:.2f}" ) plot_irradiance(city, local_datetimes, values, sun_times)