Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/examples/quality/plot_bsrn_closure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
BSRN closure test
==================

The BSRN closure test compares measured GHI against GHI computed from its
components, DHI + DNI·cos(Z), to check the consistency of the three
irradiance measurements.
"""

# %%
# In the example below, measured GHI is plotted against calculated GHI using
# :py:func:`solarpy.plotting.plot_bsrn_closure`. Reference closure limits of
# ±8% (dashed) and ±15% (dash-dot) are overlaid. Measurements falling
# outside these bands indicate a possible inconsistency in at least one of
# the components.

# %%
# Load example data
# -----------------
#
# The example data is from DTU's station in Lyngby, Denmark north of Copenhagen.
# The data is from 2023 and includes measurements of GHI, DHI, and DNI at a 1-minute resolution.

import matplotlib.pyplot as plt
import pvlib
import solarpy

data, meta = solarpy.iotools.read_t16(
"https://raw.githubusercontent.com/AssessingSolar/solarpy/refs/heads/main/data/LYN_2023.csv", # noqa: E501
map_variables=True,
)

# Calculate solar position
solar_position = pvlib.solarposition.get_solarposition(
data.index, latitude=meta["latitude"], longitude=meta["longitude"]
)
data["solar_zenith"] = solar_position["apparent_zenith"]

# Only daytime measurements are meaningful for the closure test
is_daytime = data["solar_zenith"] < 90
data = data[is_daytime]

# %%
# Plot the closure test (absolute difference)
# --------------------------------------------
#
# By default, GHI is plotted on the x-axis and the y-axis shows the
# absolute difference between the GHI components, DHI + DNI·cos(Z) - GHI,
# in W/m². The closure limits are sloped lines, since they are defined as
# a percentage of GHI.

fig, ax = solarpy.plotting.plot_bsrn_closure(
ghi=data["ghi"],
dhi=data["dhi"],
dni=data["dni"],
solar_zenith=data["solar_zenith"],
)

fig.tight_layout()

# %%
# Plot the closure test (relative difference)
# --------------------------------------------
#
# Setting ``relative=True`` plots the solar zenith angle on the x-axis and
# the ratio GHI / (DHI + DNI·cos(Z)) on the y-axis. The closure limits widen
# from ±8% to ±15% above a zenith angle of 75°, reflecting the larger
# measurement uncertainty at low sun elevations.

fig, ax = solarpy.plotting.plot_bsrn_closure(
ghi=data["ghi"],
dhi=data["dhi"],
dni=data["dni"],
solar_zenith=data["solar_zenith"],
relative=True,
)

fig.tight_layout()
1 change: 1 addition & 0 deletions docs/source/api/plotting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Plotting
plotting.two_part_colormap
plotting.irradiance_colormap_and_norm
plotting.plot_bsrn_limits
plotting.plot_bsrn_closure
plotting.plot_google_maps
plotting.plot_intraday_heatmap
plotting.plot_shading_heatmap
Expand Down
1 change: 1 addition & 0 deletions src/solarpy/plotting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from solarpy.plotting.time_drift_correlation import plot_time_drift_correlation # noqa: F401
from solarpy.plotting.multiplot import multiplot # noqa: F401
from solarpy.plotting.bsrn_limits import plot_bsrn_limits # noqa: F401
from solarpy.plotting.bsrn_comparison import plot_bsrn_closure # noqa: F401
163 changes: 163 additions & 0 deletions src/solarpy/plotting/bsrn_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Visualising solar irradiance data against the BSRN comparison checks."""

from __future__ import annotations

from typing import Any

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import TwoSlopeNorm

from solarpy.plotting.colors import two_part_colormap
from solarpy.plotting.plot_scatter import plot_scatter_heatmap


def plot_bsrn_closure(
ghi: Any,
dhi: Any,
dni: Any,
solar_zenith: Any,
relative: bool = False,
xlim: tuple[float, float] | None = None,
ylim: tuple[float, float] | None = None,
scatter_vmax: float | None = None,
cmap: Any = None,
s: float = 1.5,
bins: tuple[int, int] = (200, 200),
norm: Any = None,
ax: plt.Axes | None = None,
) -> tuple[plt.Figure, plt.Axes]:
"""Plot a scatter heatmap of the BSRN closure test.

Compares measured GHI against the GHI computed from its components,
*ghi_calc* = DHI + DNI·cos(Z), using
:func:`solarpy.plotting.plot_scatter_heatmap`. Reference closure limits
of ±8% (dashed) and ±15% (dash-dot) are overlaid.

Parameters
----------
ghi : array-like of float
Measured GHI [W/m²].
dhi : array-like of float
Measured DHI [W/m²]. Must be the same length as *ghi*.
dni : array-like of float
Measured DNI [W/m²]. Must be the same length as *ghi*.
solar_zenith : array-like of float
Solar zenith angle [°]. Must be the same length as *ghi*.
relative : bool, optional
If ``False`` (default), GHI is plotted on the x-axis and the y-axis
shows the absolute difference ``ghi_calc - ghi`` in W/m²; the
closure limits are sloped lines at ±8%/±15% of GHI. If ``True``,
the solar zenith angle is plotted on the x-axis and the y-axis
shows the ratio ``ghi / ghi_calc`` [-]; the closure limits widen
from ±8% to ±15% above a zenith angle of 75°, reflecting the
larger measurement uncertainty at low sun elevations.
xlim : tuple of float, optional
Limits of the x-axis. If ``None`` (default), ``(0, 1400)`` is used
when *relative* is ``False`` (GHI [W/m²]), and ``(0, 95)`` when
*relative* is ``True`` (solar zenith [°]).
ylim : tuple of float, optional
Limits of the y-axis. If ``None`` (default), ``(-200, 200)`` is
used when *relative* is ``False``, and ``(0.5, 1.5)`` when
*relative* is ``True``.
scatter_vmax : float, optional
Upper bound of the scatter heatmap color scale. If ``None``
(default), 175 is used when *relative* is ``False``, and 250 when
*relative* is ``True``.
cmap : matplotlib.colors.Colormap, optional
Colormap used for the scatter heatmap. If ``None`` (default),
:func:`solarpy.plotting.two_part_colormap` is used.
s : float, optional
Marker size for the scatter heatmap. Default is 1.5.
bins : tuple of int, optional
Number of bins for the scatter heatmap in the x and y directions.
Default is ``(200, 200)``.
norm : matplotlib.colors.Normalize, optional
Normalization for the scatter heatmap. If ``None`` (default), a
:class:`matplotlib.colors.TwoSlopeNorm` with ``vmin=1``,
``vcenter=20`` (or ``40`` if *relative*), and ``vmax=scatter_vmax``
is used.
ax : matplotlib.axes.Axes, optional
Axes to draw the plot on. If ``None``, a new figure with a single
axis is created.

Returns
-------
fig : matplotlib.figure.Figure
The figure containing the heatmap.
ax : matplotlib.axes.Axes
The axes containing the heatmap.

See Also
--------
solarpy.plotting.plot_bsrn_limits
"""
if ax is None:
fig, ax = plt.subplots()
else:
fig = ax.figure

if cmap is None:
cmap = two_part_colormap()

cos_sza = np.clip(np.cos(np.deg2rad(solar_zenith)), 0, None)
ghi_calc = np.clip(dhi, 0, None) + np.clip(dni, 0, None) * cos_sza

if relative:
x, y = solar_zenith, ghi / ghi_calc
xlim = (0, 95) if xlim is None else xlim
ylim = (0.5, 1.5) if ylim is None else ylim
scatter_vmax = 250 if scatter_vmax is None else scatter_vmax
vcenter = 40
xlabel, ylabel = "Solar zenith [°]", "GHI / (DHI + DNI·cos(Z)) [-]"
else:
x, y = ghi, ghi_calc - ghi
xlim = (0, 1400) if xlim is None else xlim
ylim = (-200, 200) if ylim is None else ylim
scatter_vmax = 175 if scatter_vmax is None else scatter_vmax
vcenter = 20
xlabel, ylabel = "GHI [W/m²]", "DHI + DNI·cos(Z) - GHI [W/m²]"

if norm is None:
norm = TwoSlopeNorm(vmin=1, vcenter=vcenter, vmax=scatter_vmax)

plot_scatter_heatmap(
x=x,
y=y,
ax=ax,
xlim=xlim,
ylim=ylim,
s=s,
xbins=bins[0],
ybins=bins[1],
sort_points=True,
cmap=cmap,
norm=norm,
)

limit_line_params = {"lw": 1.5, "alpha": 0.8, "c": "r", "linestyle": "--"}
if relative:
ax.plot(
[0, 75, 75, 93, 93], [1.08, 1.08, 1.15, 1.15, 1], **limit_line_params
)
ax.plot(
[0, 75, 75, 93, 93], [0.92, 0.92, 0.85, 0.85, 1], **limit_line_params
)
else:
x_limits = np.array([max(xlim[0], 50), xlim[1]])
for frac, linestyle in zip([0.08, 0.15], ["--", "-."]):
ax.plot(
x_limits,
frac * x_limits,
**{**limit_line_params, "linestyle": linestyle},
)
ax.plot(
x_limits,
-frac * x_limits,
**{**limit_line_params, "linestyle": linestyle},
)

ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)

return fig, ax
1 change: 1 addition & 0 deletions src/solarpy/plotting/bsrn_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def plot_bsrn_limits(
--------
solarpy.quality.bsrn_limits
solarpy.quality.bsrn_limits_flag
solarpy.plotting.plot_bsrn_closure
"""
if ax is None:
fig, ax = plt.subplots()
Expand Down
Loading