Source code for bsrn.visualization.clearsky_models

"""
Clear-sky model comparison booklet: one page per day, three panels (GHI, BNI, DHI).
"""

import pandas as pd
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib.pyplot as plt
from plotnine import (
    aes,
    element_text,
    facet_wrap,
    geom_line,
    ggplot,
    labs,
    scale_color_manual,
    scale_linetype_manual,
    scale_x_datetime,
    theme,
    theme_minimal,
)

from bsrn.constants import WONG_PALETTE
from bsrn.dataset import BSRNDataset
from bsrn.modeling.clear_sky import add_clearsky_columns


[docs] def plot_clearsky_models_booklet( file_path, output_file, station_code, mcclear_email=None, df=None, title=None, ): # noqa: PLR0913 """ Generate a monthly PDF booklet comparing measured irradiance with clear-sky models [1]_ [2]_ [3]_. One page is created per day. Each page has three panels for GHI, BNI, and DHI. Measured series is black solid; Ineichen, McClear, and REST2 are Wong colors and dashed. Parameters ---------- file_path : str Path to one BSRN station-to-archive monthly file (.dat.gz). output_file : str Output PDF path. station_code : str BSRN station abbreviation, used by clear-sky model calls. mcclear_email : str, optional Email required by McClear API. df : pd.DataFrame, optional If provided, use this DataFrame instead of reading from file_path. title : str, optional Same title on every PDF page. If None (default), no plot title. Returns ------- None Save booklet to `output_file`. References ---------- .. [1] Ineichen, P., & Perez, R. (2002). A new airmass independent formulation for the Linke turbidity coefficient. Solar Energy, 73(3), 151-157. .. [2] Lefèvre, M., Oumbe, A., Blanc, P., Espinar, B., Gschwind, B., Qu, Z., et al. (2013). McClear: A new model estimating downwelling solar radiation at ground level in clear-sky conditions. Atmospheric Measurement Techniques, 6(9), 2403-2418. .. [3] Gueymard, C. A. (2008). REST2: High-performance solar radiation model for cloudless-sky irradiance, illuminance, and photosynthetically active radiation: Validation with a benchmark dataset. Solar Energy, 82(3), 272-285. """ if df is None: df = BSRNDataset.from_file(file_path).data() df = df.sort_index() unique_months = df.index.to_period("M").unique() if len(unique_months) != 1: raise ValueError(f"Input must contain exactly one month. Found: {unique_months}") required = {"ghi", "bni", "dhi"} missing = required - set(df.columns) if missing: raise ValueError(f"Input data missing required columns: {sorted(missing)}") # Keep only measured core variables for model calls. base = df[["ghi", "bni", "dhi"]].copy() # Run clear-sky models. McClear optional when mcclear_email is None. out_ineichen = add_clearsky_columns(base.copy(), station_code=station_code, model="ineichen") if mcclear_email: out_mcclear = add_clearsky_columns( base.copy(), station_code=station_code, model="mcclear", mcclear_email=mcclear_email, ) else: out_mcclear = None try: out_rest2 = add_clearsky_columns( base.copy(), station_code=station_code, model="rest2" ) except FileNotFoundError: out_rest2 = None # Build model list and style maps models = ["Measured", "Ineichen"] if out_mcclear is not None: models.append("McClear") if out_rest2 is not None: models.append("REST2") width_inch = 160 / 25.4 height_inch = width_inch / 3 * 1 / 1.4 param_order = ["GHI", "BNI", "DHI"] model_order = models color_map = { "Measured": "#000000", "Ineichen": WONG_PALETTE[0], "McClear": WONG_PALETTE[1], "REST2": WONG_PALETTE[2], } linetype_map = { "Measured": "solid", "Ineichen": "dashed", "McClear": "dashed", "REST2": "dashed", } print(f"Generating clear-sky comparison booklet: {output_file} ...") with PdfPages(output_file) as pdf: for date, day_base in base.groupby(base.index.date): day_ineichen = out_ineichen.loc[day_base.index] plot_dict = { "time": day_base.index, "ghi_measured": day_base["ghi"].to_numpy(dtype=float), "bni_measured": day_base["bni"].to_numpy(dtype=float), "dhi_measured": day_base["dhi"].to_numpy(dtype=float), "ghi_ineichen": day_ineichen["ghi_clear"].to_numpy(dtype=float), "bni_ineichen": day_ineichen["bni_clear"].to_numpy(dtype=float), "dhi_ineichen": day_ineichen["dhi_clear"].to_numpy(dtype=float), } ghi_vars = ["ghi_measured", "ghi_ineichen"] bni_vars = ["bni_measured", "bni_ineichen"] dhi_vars = ["dhi_measured", "dhi_ineichen"] if out_mcclear is not None: day_mcclear = out_mcclear.loc[day_base.index] plot_dict["ghi_mcclear"] = day_mcclear["ghi_clear"].to_numpy(dtype=float) plot_dict["bni_mcclear"] = day_mcclear["bni_clear"].to_numpy(dtype=float) plot_dict["dhi_mcclear"] = day_mcclear["dhi_clear"].to_numpy(dtype=float) ghi_vars.append("ghi_mcclear") bni_vars.append("bni_mcclear") dhi_vars.append("dhi_mcclear") if out_rest2 is not None: day_rest2 = out_rest2.loc[day_base.index] plot_dict["ghi_rest2"] = day_rest2["ghi_clear"].to_numpy(dtype=float) plot_dict["bni_rest2"] = day_rest2["bni_clear"].to_numpy(dtype=float) plot_dict["dhi_rest2"] = day_rest2["dhi_clear"].to_numpy(dtype=float) ghi_vars.append("ghi_rest2") bni_vars.append("bni_rest2") dhi_vars.append("dhi_rest2") value_vars = ghi_vars + bni_vars + dhi_vars day_plot = pd.DataFrame(plot_dict) long_df = day_plot.melt( id_vars=["time"], value_vars=value_vars, var_name="series", value_name="value", ) split = long_df["series"].str.split("_", n=1, expand=True) long_df["parameter"] = split[0].str.upper() long_df["model"] = split[1].replace( {"measured": "Measured", "ineichen": "Ineichen", "mcclear": "McClear", "rest2": "REST2"} ) long_df["parameter"] = pd.Categorical(long_df["parameter"], categories=param_order, ordered=True) long_df["model"] = pd.Categorical(long_df["model"], categories=model_order, ordered=True) used_colors = {m: color_map[m] for m in model_order if m in color_map} used_linetypes = {m: linetype_map[m] for m in model_order if m in linetype_map} _lab = {"x": "Time (UTC)", "y": "[W/m²]", "color": "", "linetype": ""} if title is not None: _lab["title"] = title p = ( ggplot(long_df, aes(x="time", y="value", color="model", linetype="model")) + geom_line(size=0.3) + facet_wrap("~parameter", nrow=1, ncol=3, scales="free_y") + scale_color_manual(values=used_colors) + scale_linetype_manual(values=used_linetypes) + scale_x_datetime(date_labels="%H:%M") + labs(**_lab) + theme_minimal() + theme( text=element_text(family="Times New Roman", size=9), axis_title=element_text(size=9), axis_text=element_text(size=9), plot_title=element_text(size=9), strip_text=element_text(size=9), figure_size=(width_inch, height_inch), legend_position="top", ) ) fig = p.draw() pdf.savefig(fig, bbox_inches="tight") plt.close(fig) print("Done.")