BUFR - Skew-T with Parcel Path and Hodograph

BUFR - Skew-T with Parcel Path and Hodograph
# (C) Copyright 2017- ECMWF.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.
#

import metview as mv
import numpy as np

# Note: at least Metview version 5.17.0 is required


def build_title_text(prof):
    """
    Utility function to generate text for plot title
    """

    # get profile info for title
    info = mv.thermo_data_info(prof)

    # obs
    if "station" in info:
        t1 = "{} {} UTC WMO Id: {}".format(
            int(info["date"]), int(info["time"]), int(info["station"])
        )
    # fc or an
    else:
        t1 = "Run: {} UTC +{}h Valid: {} UTC".format(
            info["base_date"].strftime("%Y-%m-%d %H"),
            int(info["step"]),
            info["valid_date"].strftime("%Y-%m-%d %H"),
        )

    t2 = "Lat: {:.2f} Lon: {:.2f}".format(info["lat"], info["lon"])
    return [t1, t2]


def build_box_text(p):
    """
    Utility function to generate text for parcel info box
    """

    def _add_row(label, val, units=""):
        t = "{:>7} ".format(label + ":")
        if val is None:
            t += "-"
        elif isinstance(val, str):
            t += "{:<}".format(val)
        else:
            t += "{:.0f} {}".format(val, units)
        return t

    def _add_row_pt(label, val):
        t = "{:>7} ".format(label + ":")
        if val is None or not isinstance(val, dict):
            t += "-"
        else:
            t += "{:.0f}/{:.1f}".format(val["p"], val["t"])
        return t

    t = []
    t.append("{:>7} {:<10}".format("mode:", p["start"]["mode"]))
    t.append(_add_row("cape", p.get("cape"), "J/kg"))
    t.append(_add_row("cin", p.get("cin"), "J/kg"))
    t.append(_add_row("li", p.get("li"), ""))
    t.append(_add_row_pt("lcl", p.get("lcl")))
    t.append(_add_row_pt("lfc", p.get("lfc")))
    t.append(_add_row_pt("el", p.get("el")))
    t.append(_add_row_pt("start", p.get("start")))

    return t


def build_layout(thermo_view, hodo_incr=5, hodo_max_val=35):
    """
    Utility function to build a layout with thermo view on the left
    and hodograph on the right
    """

    # --------------------------------
    # define page for the thermo view
    # --------------------------------

    th_page = mv.plot_page(
        top=0,
        bottom=100,
        left=0,
        right=75,
        view=thermo_view,
    )

    # ----------------------------
    # define the hodograph view
    # ----------------------------

    # the maximum radial size of the coordinate system
    hodo_max_val = np.ceil(hodo_max_val / hodo_incr) * hodo_incr

    # define horizontal and vertical  axes
    h_axis = mv.maxis(axis_position="left", axis_tick="off", axis_tick_label="off")
    v_axis = mv.maxis(axis_position="bottom", axis_tick="off", axis_tick_label="off")

    # the view
    view = mv.cartesianview(
        x_automatic="off",
        x_min=-hodo_max_val,
        x_max=hodo_max_val,
        y_automatic="off",
        y_min=-hodo_max_val,
        y_max=hodo_max_val,
        horizontal_axis=h_axis,
        vertical_axis=v_axis,
        subpage_frame="on",
        subpage_frame_thickness=1,
        subpage_x_position=5,
        subpage_y_position=5,
        subpage_x_length=90,
        subpage_y_length=90,
    )

    # define the hodograph plot page.
    # NOTE: In order to correctly render the hodograph (we want
    # concentric circles instead of ellipses) we have to ensure
    # that the physical width and height of the plot are the same.
    # Please note that while the page size is defined in % the
    # superpage size is defined in cm! See also subpage size in the view.

    # physical size of the whole plot (A4 landscape)
    sp_width = 29.7
    sp_height = 21

    hodo_width = 7  # cm
    hodo_height = 7  # cm
    page_width = 100 * hodo_width / sp_width
    page_height = 100 * hodo_height / sp_height
    page_top = 10
    page_left = 100 - page_width - 5

    hodo_page = mv.plot_page(
        top=page_top,
        bottom=page_top + page_height,
        left=page_left,
        right=page_left + page_width,
        view=view,
    )

    # ---------------------------------------
    # define the superpage (A4 landscape)
    # ---------------------------------------

    dw = mv.plot_superpage(pages=[th_page, hodo_page])
    return dw


def build_hodo_bg(
    hodo_incr=5,
    hodo_max_val=35,
    hodo_highlight=[10, 20, 30],
    hodo_label=[10, 20, 30],
    hodo_label_size=0.3,
    hodo_colour="black",
):
    """
    Utility function to generate plot objects making up
    the hodograph background
    """

    # the maximum radial size of the coordinate system
    hodo_max_val = np.ceil(hodo_max_val / hodo_incr) * hodo_incr

    gr_lst = []

    # build the concentric circles
    sp = hodo_incr
    angle_incr = 2 * np.pi / 180
    while sp <= hodo_max_val:
        xp = [np.cos(i * angle_incr) * sp for i in range(1, 182)]
        yp = [np.sin(i * angle_incr) * sp for i in range(1, 182)]
        gr = mv.xy_curve(xp, yp, hodo_colour, "solid", 1 if sp in hodo_highlight else 1)
        gr_lst.append(gr)
        sp += hodo_incr

    # build horizontal and vertical lines going
    # through the centre
    gr_lst.append(
        mv.xy_curve([-hodo_max_val, hodo_max_val], [0, 0], hodo_colour, "solid", 1)
    )
    gr_lst.append(
        mv.xy_curve([0, 0], [-hodo_max_val, hodo_max_val], hodo_colour, "solid", 1)
    )

    # add labels to the horizontal line
    vis = mv.input_visualiser(
        input_plot_type="xy_point",
        input_x_values=[-v for v in hodo_label] + hodo_label,
        input_y_values=[0] * 2 * len(hodo_label),
        input_values=hodo_label + hodo_label,
    )

    sym = mv.msymb(
        symbol_colour=hodo_colour,
        symbol_text_font_size=hodo_label_size,
        symbol_text_font_style="normal",
        symbol_text_position="bottom",
    )

    gr_lst.extend([vis, sym])

    return gr_lst


def build_hodo_wind(prof, pres_bins, pres_colours):
    """
    Utility function to generate plot objects for the hodograph
    wind data (per bin)
    """

    # get individual profiles as vectors. Values are sorted by descending
    # pressure, no missing values includes.
    info = mv.thermo_data_values(prof, 0)
    p = info["p_wind"]
    u = info["u"]
    v = info["v"]

    gr_wind = []
    for i in range(len(pres_bins) - 1):

        # collect wind data in bin
        u_val = []
        v_val = []
        for k in range(len(p)):
            if (
                not np.isnan(p[k])
                and not np.isnan(u[k])
                and not np.isnan(v[k])
                and p[k] <= pres_bins[i]
                and p[k] >= pres_bins[i + 1]
            ):
                u_val.append(u[k])
                v_val.append(v[k])

        # build graph object
        if u_val and v_val:
            vis = mv.input_visualiser(input_x_values=u_val, input_y_values=v_val)

            gr = mv.mgraph(
                legend="on",
                graph_line_colour=pres_colours[i],
                graph_line_style="solid",
                graph_line_thickness=5,
            )
            gr_wind.extend([vis, gr])

    return gr_wind


# read BUFR data
filename = "temp.bufr"
if mv.exist(filename):
    b = mv.read(filename)
else:
    b = mv.gallery.load_dataset(filename)

# define station id
statid = "78583"

# extract thermo profile
prof = mv.thermo_bufr(data=b, station=mv.stations(search_key="ident", ident=statid))

# -----------------------------
# profile and parcel path
# -----------------------------

# compute parcel path
parcel = mv.thermo_parcel_path(prof, mode="mucape", layer_depth=300)

# create plot object for parcel areas
parcel_area = mv.thermo_parcel_area(parcel)

# create plot object for parcel path
parcel_vis = mv.xy_curve(parcel["t"], parcel["p"], "charcoal", "dash", 6)

# define t and td profile style
prof_vis = mv.mthermo(
    thermo_temperature_line_thickness=5,
    thermo_temperature_missing_data_thickness=5,
    thermo_dewpoint_line_thickness=5,
    thermo_dewpoint_missing_data_thickness=5,
)

# define wind plotting style
prof_wind_style = mv.mwind(
    wind_thinning_factor=1,
    wind_field_type="flags",
    wind_flag_colour="magenta",
    wind_flag_length=0.8,
    wind_flag_origin_marker="dot",
    wind_flag_origin_marker_size=0.2,
)

# define thermo grid
thermo_grid = mv.mthermogrid(
    thermo_isotherm_colour="RGB(0.2577,0.6364,0.5039)",
    thermo_isotherm_reference_colour="blue",
    thermo_dry_adiabatic_colour="grey",
    thermo_dry_adiabatic_label_frequency=2,
    thermo_mixing_ratio_colour="RGB(0.2577,0.6364,0.5039)",
    thermo_mixing_ratio_label_colour="RGB(0.2577,0.6364,0.5039)",
    thermo_mixing_ratio_label_font_size=0.4,
)

# define thermodynamic view
thermo_view = mv.thermoview(
    type="skewt",
    minimum_temperature=-140,
    maximum_temperature=40,
    top_pressure=100,
    thermo_grid=thermo_grid,
    subpage_clipping="on",
    subpage_x_position=8,
    subpage_y_position=15,
    subpage_x_length=82,
    subpage_y_length=82,
)

# define profile title
prof_title_txt = build_title_text(prof)
prof_title = mv.mtext(
    text_lines=prof_title_txt, text_font_size=0.5, text_colour="charcoal"
)

# -----------------------------
# hodograph
# -----------------------------

# hodograph options
hodo_incr = 5
hodo_highlight = [10, 20, 30]
hodo_label = [10, 20, 30]
hodo_max_val = 35
hodo_colour = "RGB(0.4,0.4,0.4)"

# define the wind speed bins and their associated colours
pres_bins = [1050, 700, 500, 300, 200, 100]
pres_colours = ["red", "kelly_green", "sky", "blue", "magenta"]

# generate the graphical objects for the hodograph background
gr_hodo_bg = build_hodo_bg(
    hodo_incr=hodo_incr,
    hodo_highlight=hodo_highlight,
    hodo_label=hodo_label,
    hodo_max_val=hodo_max_val,
    hodo_colour=hodo_colour,
)

# generate the graphical objects for wind data on the hodograph
gr_hodo_wind = build_hodo_wind(prof, pres_bins, pres_colours)

# define hodograph legend with custom labels
legend_text = []
for i in range(len(pres_bins) - 1):
    legend_text.append(str(pres_bins[i]) + "-" + str(pres_bins[i + 1]))

hodo_legend = mv.mlegend(
    legend_display_type="disjoint",
    legend_box_mode="positional",
    legend_text_font_size=0.3,
    legend_text_composition="user_text_only",
    legend_user_lines=legend_text,
    legend_column_count=3,
    legend_box_y_position=6.5,
    legend_box_y_length=1.5,
    legend_entry_text_width=80,
)

# -----------------------------
# layout
# -----------------------------

# generate the layout and build the view for the hodograph
dw = build_layout(thermo_view, hodo_incr=hodo_incr, hodo_max_val=hodo_max_val)

# -------------------------------
# info box
# -------------------------------

txt = build_box_text(parcel)

# create info box - ensure font is monospace
info_box = mv.mtext(
    text_lines=txt,
    text_font="courier",
    text_font_size=0.45,
    text_colour="charcoal",
    text_justification="left",
    text_mode="positional",
    text_box_x_position=22.1,
    text_box_y_position=6.8,
    text_box_x_length=4.8,
    text_box_y_length=len(txt) * 0.5 + 0.5,
    text_box_blanking="on",
    text_border="on",
    text_border_colour="charcoal",
)

# define the output plot file
mv.setoutput(mv.pdf_output(output_name="skewt_parcel_path_with_hodograph"))

# generate the plot
mv.plot(
    dw[0],
    parcel_area,
    prof,
    prof_vis,
    prof_wind_style,
    parcel_vis,
    prof_title,
    info_box,
    dw[1],
    gr_hodo_bg,
    gr_hodo_wind,
    hodo_legend,
)