Files
cariflex/tools/flexmeasures-weather/scripts/solartest.py
Eric F d4974e3241 Add FlexMeasures plugins, USEF protocol, and Cariflex simulator
- flexmeasures-entsoe: ENTSO-E data plugin
- flexmeasures-weather: Weather data plugin
- USEF Flex Trading Protocol PDF (2.4MB)
- Cariflex simulator (publishes to Redis)
- Dashboard Grafana updated with correct InfluxDB queries
- All tools extracted in /tools/
2026-06-08 07:38:57 -04:00

178 lines
5.7 KiB
Python
Executable File

#!/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)