Source code for tmhp.mollier_diagram

"""
Mollier diagram visualization functions.
"""

from functools import lru_cache
from typing import cast

import CoolProp.CoolProp as CP
import dartwork_mpl as dm
import matplotlib.axes as maxes
import matplotlib.figure as mfigure
import numpy as np

from . import calc_util as cu


def _draw_cycle_lines_and_annotations(
    ax: maxes.Axes,
    pts_x: dict[str, float],
    pts_y: dict[str, float],
    is_on: bool,
    color_sat_liq: str,
    color_sat_vap: str,
    line_color: str = "oc.gray5",
    color_on: str = "black",
    color_off: str = "oc.gray6",
    tol_atol: float = 0.1,
    tol_y_atol: float | None = None,
    point_labels: dict[str, str] | None = None,
) -> None:
    """Draw heat pump cycle lines, scatter markers, and aligned text annotations."""
    cycle_markerfacecolor = color_on if is_on else color_off
    cycle_markeredgecolor = color_on if is_on else color_off

    _tol_y = tol_y_atol if tol_y_atol is not None else tol_atol

    def points_are_close(x1, y1, x2, y2, tol=tol_atol):
        if np.isnan(x1) or np.isnan(y1) or np.isnan(x2) or np.isnan(y2):
            return False
        return abs(x1 - x2) < tol and abs(y1 - y2) < _tol_y

    def draw_segment(k1, k2):
        if not (np.isnan(pts_x[k1]) or np.isnan(pts_x[k2]) or np.isnan(pts_y[k1]) or np.isnan(pts_y[k2])):
            if points_are_close(pts_x[k1], pts_y[k1], pts_x[k2], pts_y[k2], tol=tol_atol):
                ax.plot(
                    pts_x[k1],
                    pts_y[k1],
                    marker="o",
                    linestyle="None",
                    markerfacecolor=cycle_markerfacecolor,
                    markeredgecolor=cycle_markeredgecolor,
                    markeredgewidth=dm.lw(0),
                    markersize=2.5,
                    zorder=1,
                )
            else:
                ax.plot(
                    [pts_x[k1], pts_x[k2]],
                    [pts_y[k1], pts_y[k2]],
                    color=line_color,
                    linewidth=dm.lw(0),
                    linestyle=":",
                    zorder=1,
                )

    draw_segment("4", "1_star")
    draw_segment("1_star", "1")
    draw_segment("1", "2")
    draw_segment("2", "2_star")
    draw_segment("2_star", "3_star")
    draw_segment("3_star", "3")
    draw_segment("3", "4")

    points_list = []
    if points_are_close(pts_x["1_star"], pts_y["1_star"], pts_x["1"], pts_y["1"], tol_atol):
        points_list.append((pts_x["1"], pts_y["1"], "1"))
    else:
        if not (np.isnan(pts_x["1_star"]) or np.isnan(pts_y["1_star"])):
            points_list.append((pts_x["1_star"], pts_y["1_star"], "1'"))
        if not (np.isnan(pts_x["1"]) or np.isnan(pts_y["1"])):
            points_list.append((pts_x["1"], pts_y["1"], "1"))

    if points_are_close(pts_x["2"], pts_y["2"], pts_x["2_star"], pts_y["2_star"], tol_atol):
        points_list.append((pts_x["2"], pts_y["2"], "2"))
    else:
        if not (np.isnan(pts_x["2"]) or np.isnan(pts_y["2"])):
            points_list.append((pts_x["2"], pts_y["2"], "2"))
        if not (np.isnan(pts_x["2_star"]) or np.isnan(pts_y["2_star"])):
            points_list.append((pts_x["2_star"], pts_y["2_star"], "2'"))

    if points_are_close(pts_x["3_star"], pts_y["3_star"], pts_x["3"], pts_y["3"], tol_atol):
        points_list.append((pts_x["3"], pts_y["3"], "3"))
    else:
        if not (np.isnan(pts_x["3_star"]) or np.isnan(pts_y["3_star"])):
            points_list.append((pts_x["3_star"], pts_y["3_star"], "3'"))
        if not (np.isnan(pts_x["3"]) or np.isnan(pts_y["3"])):
            points_list.append((pts_x["3"], pts_y["3"], "3"))

    if not (np.isnan(pts_x["4"]) or np.isnan(pts_y["4"])):
        points_list.append((pts_x["4"], pts_y["4"], "4"))

    text_cfg = {
        "1": (4, 0, "left", "center"),
        "2": (4, 0, "left", "center"),
        "3": (-4, 0, "right", "center"),
        "4": (-4, 0, "right", "center"),
    }

    if point_labels is None:
        point_labels = {"1": "cmp,in", "2": "cmp,out", "3": "exp,in", "4": "exp,out"}

    fig = ax.figure

    for x_val, y_val, key in points_list:
        if key in ["1'", "2'"]:
            ax.plot(
                x_val,
                y_val,
                marker="o",
                markersize=2.5,
                markerfacecolor="white",
                markeredgecolor=color_sat_vap,
                markeredgewidth=dm.lw(0),
                zorder=2,
            )
            continue
        elif key == "3'":
            ax.plot(
                x_val,
                y_val,
                marker="o",
                markersize=2.5,
                markerfacecolor="white",
                markeredgecolor=color_sat_liq,
                markeredgewidth=dm.lw(0),
                zorder=2,
            )
            continue

        ax.plot(
            x_val,
            y_val,
            marker="o",
            markersize=2.5,
            markerfacecolor=cycle_markerfacecolor,
            markeredgecolor=cycle_markeredgecolor,
            markeredgewidth=dm.lw(0),
            zorder=2,
        )

        dx, dy, ha, va = text_cfg.get(key, (0, 4, "center", "bottom"))
        text_str = str(point_labels.get(key, key))

        if fig is not None:
            cfig = cast(mfigure.Figure, fig)
            offset = dm.make_offset(dx, dy, cfig)
            ax.text(x_val, y_val, text_str, transform=ax.transData + offset, ha=ha, va=va, fontsize=dm.fs(-2))
        else:
            ax.annotate(
                text_str, (x_val, y_val), xytext=(dx, dy), textcoords="offset points", ha=ha, va=va, fontsize=dm.fs(-2)
            )


@lru_cache(maxsize=8)
def _get_saturation_curves(refrigerant: str):
    T_min = CP.PropsSI("Tmin", refrigerant)
    T_crit = CP.PropsSI("Tcrit", refrigerant)
    temps_K = np.linspace(T_min + 1, T_crit - 0.5, 10000)
    temps = [cu.K2C(T) for T in temps_K]
    h_liq = [CP.PropsSI("H", "T", T, "Q", 0, refrigerant) / 1000 for T in temps_K]
    h_vap = [CP.PropsSI("H", "T", T, "Q", 1, refrigerant) / 1000 for T in temps_K]
    p_sat = [CP.PropsSI("P", "T", T, "Q", 0, refrigerant) / 1000 for T in temps_K]
    s_liq = [CP.PropsSI("S", "T", T, "Q", 0, refrigerant) / 1000 for T in temps_K]
    s_vap = [CP.PropsSI("S", "T", T, "Q", 1, refrigerant) / 1000 for T in temps_K]
    try:
        h_crit = CP.PropsSI("H", "T", T_crit, "Q", 0, refrigerant) / 1000
        p_crit = CP.PropsSI("P", "T", T_crit, "Q", 0, refrigerant) / 1000
        s_crit = CP.PropsSI("S", "T", T_crit, "Q", 0, refrigerant) / 1000
        temps.append(cu.K2C(T_crit))
        h_liq.append(h_crit)
        h_vap.append(h_crit)
        p_sat.append(p_crit)
        s_liq.append(s_crit)
        s_vap.append(s_crit)
    except Exception:
        pass
    return temps, h_liq, h_vap, p_sat, s_liq, s_vap


REF_LIMITS: dict[str, dict[str, dict[str, float]]] = {
    "R410A": {
        "th": {"xmin": 0.0, "xmax": 700.0, "ymin": -40.0, "ymax": 140.0},
        "ph": {"xmin": 0.0, "xmax": 700.0, "ymin": 100.0, "ymax": 10000.0},
        "ts": {"xmin": 0.0, "xmax": 3.0, "ymin": -40.0, "ymax": 140.0},
    },
    "R134a": {
        "th": {"xmin": 0.0, "xmax": 600.0, "ymin": -40.0, "ymax": 120.0},
        "ph": {"xmin": 0.0, "xmax": 600.0, "ymin": 100.0, "ymax": 10000.0},
        "ts": {"xmin": 0.0, "xmax": 2.5, "ymin": -40.0, "ymax": 120.0},
    },
    "R32": {
        "th": {"xmin": 0.0, "xmax": 800.0, "ymin": -40.0, "ymax": 200.0},
        "ph": {"xmin": 0.0, "xmax": 800.0, "ymin": 100.0, "ymax": 10000.0},
        "ts": {"xmin": 0.0, "xmax": 3.0, "ymin": -40.0, "ymax": 200.0},
    },
    "R290": {
        "th": {"xmin": 0.0, "xmax": 850.0, "ymin": -40.0, "ymax": 140.0},
        "ph": {"xmin": 0.0, "xmax": 850.0, "ymin": 100.0, "ymax": 10000.0},
        "ts": {"xmin": 0.0, "xmax": 3.0, "ymin": -40.0, "ymax": 140.0},
    },
}


[docs] def plot_th_diagram( ax: maxes.Axes, result: dict[str, float], refrigerant: str, T_cond_bound: dict[str, float | str] | None = None, T_evap_bound: dict[str, float | str] | None = None, fontsize: float | None = None, tick_pad: float | None = None, sat_liquid_label: str = "Sat. liquid", sat_vapor_label: str = "Sat. vapor", ref_cycle_label: str = "Ref. cycle", point_labels: dict[str, str] | None = None, ) -> None: """Plot T-h diagram on given axis.""" color1, color2, color3, color4, line_color = "oc.blue5", "oc.red5", "black", "oc.gray6", "oc.gray5" limits = REF_LIMITS.get(refrigerant, {"th": {"xmin": 200.0, "xmax": 750.0, "ymin": -40.0, "ymax": 160.0}})["th"] temps, h_liq, h_vap, _, _, _ = _get_saturation_curves(refrigerant) h1_star = result.get("h_ref_evap_sat [J/kg]", result.get("h1_star [J/kg]", np.nan)) * cu.J2kJ h1 = result.get("h_ref_cmp_in [J/kg]", result.get("h1 [J/kg]", np.nan)) * cu.J2kJ h2 = result.get("h_ref_cmp_out [J/kg]", result.get("h2 [J/kg]", np.nan)) * cu.J2kJ h2_star = result.get("h_ref_cond_sat_v [J/kg]", result.get("h2_star [J/kg]", np.nan)) * cu.J2kJ h3_star = result.get("h_ref_cond_sat_l [J/kg]", result.get("h3_star [J/kg]", np.nan)) * cu.J2kJ h3 = result.get("h_ref_exp_in [J/kg]", result.get("h3 [J/kg]", np.nan)) * cu.J2kJ h4 = result.get("h_ref_exp_out [J/kg]", result.get("h4 [J/kg]", np.nan)) * cu.J2kJ T1_star = result.get("T_ref_evap_sat [°C]", result.get("T1_star [°C]", np.nan)) T1 = result.get("T_ref_cmp_in [°C]", result.get("T1 [°C]", np.nan)) T2 = result.get("T_ref_cmp_out [°C]", result.get("T2 [°C]", np.nan)) T2_star = result.get("T_ref_cond_sat_v [°C]", result.get("T2_star [°C]", np.nan)) T3_star = result.get("T_ref_cond_sat_l [°C]", result.get("T3_star [°C]", np.nan)) T3 = result.get("T_ref_exp_in [°C]", result.get("T3 [°C]", np.nan)) T4 = result.get("T_ref_exp_out [°C]", result.get("T4 [°C]", np.nan)) if np.isnan(h1_star) and not np.isnan(h1): h1_star, T1_star = h1, T1 if np.isnan(h3_star) and not np.isnan(h3): h3_star, T3_star = h3, T3 ax.plot(h_liq, temps, color=color1, label=sat_liquid_label, linewidth=dm.lw(0)) ax.plot(h_vap, temps, color=color2, label=sat_vapor_label, linewidth=dm.lw(0)) pts_x = {"1_star": h1_star, "1": h1, "2": h2, "2_star": h2_star, "3_star": h3_star, "3": h3, "4": h4} pts_y = {"1_star": T1_star, "1": T1, "2": T2, "2_star": T2_star, "3_star": T3_star, "3": T3, "4": T4} is_on = bool(result.get("hp_is_on", result.get("is_on", False))) # T-h chart: x-axis = enthalpy [kJ/kg], y-axis = temperature [°C]. # The two axes use different units, so tol_atol and tol_y_atol are # specified independently: # - tol_atol = 0.5 kJ/kg — narrow enthalpy difference inside the # saturation dome. # - tol_y_atol = 0.5 °C — temperature resolution that lets the SH/SC # (superheat / subcool) segments stay visible. _draw_cycle_lines_and_annotations( ax, pts_x, pts_y, is_on, color1, color2, line_color, color3, color4, tol_atol=0.5, tol_y_atol=0.5, point_labels=point_labels, ) trans = ax.get_yaxis_transform() if T_cond_bound is not None: val = T_cond_bound.get("val") label = T_cond_bound.get("label", "Cond_bound") if val is not None and not (isinstance(val, float) and np.isnan(val)): val = float(val) # type: ignore dy = float(T_cond_bound.get("dy", 2.0)) va = T_cond_bound.get("va", "bottom") ax.axhline(y=val, color="oc.red5", linestyle=":", linewidth=dm.lw(0)) offset = dm.make_offset(4, float(dy), cast(mfigure.Figure, ax.figure)) if ax.figure else None transform = trans + offset if offset else trans ax.text( 0.0, val, f"{label}: {val:.1f}°C", color="oc.red5", ha="left", va=va, transform=transform, fontsize=dm.fs(-2), ) if T_evap_bound is not None: val = T_evap_bound.get("val") label = T_evap_bound.get("label", "Evap_bound") if val is not None and not (isinstance(val, float) and np.isnan(val)): val = float(val) # type: ignore dy = float(T_evap_bound.get("dy", -2.0)) va = T_evap_bound.get("va", "top") ax.axhline(y=val, color="oc.orange5", linestyle=":", linewidth=dm.lw(0)) offset = dm.make_offset(4, float(dy), cast(mfigure.Figure, ax.figure)) if ax.figure else None transform = trans + offset if offset else trans ax.text( 0.0, val, f"{label}: {val:.1f}°C", color="oc.orange5", ha="left", va=va, transform=transform, fontsize=dm.fs(-2), ) ax.set_xlabel("Enthalpy [kJ/kg]") ax.set_ylabel("Temperature [°C]") ax.set_xlim(limits["xmin"], limits["xmax"]) ax.set_ylim(limits["ymin"], limits["ymax"]) import matplotlib.ticker as ticker if refrigerant == "R32": ax.yaxis.set_major_locator(ticker.MultipleLocator(40)) legend_handles = [ ax.plot([], [], color=color1, linewidth=dm.lw(0), label=sat_liquid_label)[0], ax.plot([], [], color=color2, linewidth=dm.lw(0), label=sat_vapor_label)[0], ax.plot( [], [], color=line_color, linewidth=dm.lw(0), marker="o", linestyle=":", markersize=2.5, markerfacecolor=color3, markeredgecolor=color3, markeredgewidth=dm.lw(0), label=ref_cycle_label, )[0], ] ax.legend( handles=legend_handles, loc="upper center", handlelength=1.5, labelspacing=0.5, columnspacing=2, ncol=3, frameon=False, )
[docs] def plot_ph_diagram( ax: maxes.Axes, result: dict[str, float], refrigerant: str, fontsize: float | None = None, tick_pad: float | None = None, sat_liquid_label: str = "Sat. liquid", sat_vapor_label: str = "Sat. vapor", ref_cycle_label: str = "Ref. cycle", point_labels: dict[str, str] | None = None, ) -> None: """Plot P-h diagram on given axis.""" color1, color2, color3, color4, line_color = "oc.blue5", "oc.red5", "black", "oc.gray6", "oc.gray4" limits = REF_LIMITS.get(refrigerant, {"ph": {"xmin": 200.0, "xmax": 750.0, "ymin": 100.0, "ymax": 10000.0}})["ph"] temps, h_liq, h_vap, _, _, _ = _get_saturation_curves(refrigerant) p_sat = [CP.PropsSI("P", "T", cu.C2K(T), "Q", 0, refrigerant) / 1000 for T in temps] P1_star = (result.get("P_ref_evap_sat [Pa]") or result.get("P1_star [Pa]", np.nan)) * cu.Pa2kPa P1 = (result.get("P_ref_cmp_in [Pa]") or result.get("P1 [Pa]", np.nan)) * cu.Pa2kPa P2 = (result.get("P_ref_cmp_out [Pa]") or result.get("P2 [Pa]", np.nan)) * cu.Pa2kPa P2_star = (result.get("P_ref_cond_sat_v [Pa]") or result.get("P2_star [Pa]", np.nan)) * cu.Pa2kPa P3_star = (result.get("P_ref_cond_sat_l [Pa]") or result.get("P3_star [Pa]", np.nan)) * cu.Pa2kPa P3 = (result.get("P_ref_exp_in [Pa]") or result.get("P3 [Pa]", np.nan)) * cu.Pa2kPa P4 = (result.get("P_ref_exp_out [Pa]") or result.get("P4 [Pa]", np.nan)) * cu.Pa2kPa h1_star = (result.get("h_ref_evap_sat [J/kg]") or result.get("h1_star [J/kg]", np.nan)) * cu.J2kJ h1 = (result.get("h_ref_cmp_in [J/kg]") or result.get("h1 [J/kg]", np.nan)) * cu.J2kJ h2 = (result.get("h_ref_cmp_out [J/kg]") or result.get("h2 [J/kg]", np.nan)) * cu.J2kJ h2_star = (result.get("h_ref_cond_sat_v [J/kg]") or result.get("h2_star [J/kg]", np.nan)) * cu.J2kJ h3_star = (result.get("h_ref_cond_sat_l [J/kg]") or result.get("h3_star [J/kg]", np.nan)) * cu.J2kJ h3 = (result.get("h_ref_exp_in [J/kg]") or result.get("h3 [J/kg]", np.nan)) * cu.J2kJ h4 = (result.get("h_ref_exp_out [J/kg]") or result.get("h4 [J/kg]", np.nan)) * cu.J2kJ if np.isnan(h1_star) and not np.isnan(h1): h1_star, P1_star = h1, P1 if np.isnan(h3_star) and not np.isnan(h3): h3_star, P3_star = h3, P3 ax.plot(h_liq, p_sat, color=color1, label=sat_liquid_label, linewidth=dm.lw(0)) ax.plot(h_vap, p_sat, color=color2, label=sat_vapor_label, linewidth=dm.lw(0)) pts_x = {"1_star": h1_star, "1": h1, "2": h2, "2_star": h2_star, "3_star": h3_star, "3": h3, "4": h4} pts_y = {"1_star": P1_star, "1": P1, "2": P2, "2_star": P2_star, "3_star": P3_star, "3": P3, "4": P4} is_on = bool(result.get("hp_is_on", result.get("is_on", False))) _draw_cycle_lines_and_annotations( ax, pts_x, pts_y, is_on, color1, color2, line_color, color3, color4, tol_atol=0.5, tol_y_atol=None, point_labels=point_labels, ) ax.set_xlabel("Enthalpy [kJ/kg]") ax.set_ylabel("Pressure [kPa]") ax.set_xlim(limits["xmin"], limits["xmax"]) ax.set_ylim(limits["ymin"], limits["ymax"]) ax.set_yscale("log") legend_handles = [ ax.plot([], [], color=color1, linewidth=dm.lw(0), label=sat_liquid_label)[0], ax.plot([], [], color=color2, linewidth=dm.lw(0), label=sat_vapor_label)[0], ax.plot( [], [], color=line_color, linewidth=dm.lw(0), marker="o", linestyle=":", markersize=2.5, markerfacecolor=color3, markeredgecolor=color3, markeredgewidth=dm.lw(0), label=ref_cycle_label, )[0], ] ax.legend( handles=legend_handles, loc="upper center", handlelength=1.5, labelspacing=0.5, columnspacing=2, ncol=3, frameon=False, )
[docs] def plot_ts_diagram( ax: maxes.Axes, result: dict[str, float], refrigerant: str, T_cond_bound: dict[str, float | str] | None = None, T_evap_bound: dict[str, float | str] | None = None, sat_liquid_label: str = "Sat. liquid", sat_vapor_label: str = "Sat. vapor", ref_cycle_label: str = "Ref. cycle", point_labels: dict[str, str] | None = None, ) -> None: """Plot T-s diagram on given axis with super heating/cooling considered.""" color1, color2, color3, color4, line_color = "oc.blue5", "oc.red5", "black", "oc.gray6", "oc.gray5" limits = REF_LIMITS.get(refrigerant, {"ts": {"xmin": 1.0, "xmax": 3.0, "ymin": -40.0, "ymax": 160.0}})["ts"] temps, _, _, _, s_liq, s_vap = _get_saturation_curves(refrigerant) ax.plot(s_liq, temps, color=color1, label=sat_liquid_label, linewidth=dm.lw(0)) ax.plot(s_vap, temps, color=color2, label=sat_vapor_label, linewidth=dm.lw(0)) s1_star = (result.get("s_ref_evap_sat [J/(kg·K)]") or result.get("s1_star [J/(kg·K)]", np.nan)) / 1000 s1 = (result.get("s_ref_cmp_in [J/(kg·K)]") or result.get("s1 [J/(kg·K)]", np.nan)) / 1000 s2 = (result.get("s_ref_cmp_out [J/(kg·K)]") or result.get("s2 [J/(kg·K)]", np.nan)) / 1000 s2_star = (result.get("s_ref_cond_sat_v [J/(kg·K)]") or result.get("s2_star [J/(kg·K)]", np.nan)) / 1000 s3_star = (result.get("s_ref_cond_sat_l [J/(kg·K)]") or result.get("s3_star [J/(kg·K)]", np.nan)) / 1000 s3 = (result.get("s_ref_exp_in [J/(kg·K)]") or result.get("s3 [J/(kg·K)]", np.nan)) / 1000 s4 = (result.get("s_ref_exp_out [J/(kg·K)]") or result.get("s4 [J/(kg·K)]", np.nan)) / 1000 T1_star = result.get("T_ref_evap_sat [°C]") or result.get("T1_star [°C]", np.nan) T1 = result.get("T_ref_cmp_in [°C]") or result.get("T1 [°C]", np.nan) T2 = result.get("T_ref_cmp_out [°C]") or result.get("T2 [°C]", np.nan) T2_star = result.get("T_ref_cond_sat_v [°C]") or result.get("T2_star [°C]", np.nan) T3_star = result.get("T_ref_cond_sat_l [°C]") or result.get("T3_star [°C]", np.nan) T3 = result.get("T_ref_exp_in [°C]") or result.get("T3 [°C]", np.nan) T4 = result.get("T_ref_exp_out [°C]") or result.get("T4 [°C]", np.nan) if np.isnan(s1_star) and not np.isnan(s1): s1_star, T1_star = s1, T1 if np.isnan(s3_star) and not np.isnan(s3): s3_star, T3_star = s3, T3 pts_x = {"1_star": s1_star, "1": s1, "2": s2, "2_star": s2_star, "3_star": s3_star, "3": s3, "4": s4} pts_y = {"1_star": T1_star, "1": T1, "2": T2, "2_star": T2_star, "3_star": T3_star, "3": T3, "4": T4} is_on = bool(result.get("hp_is_on", result.get("is_on", False))) _draw_cycle_lines_and_annotations( ax, pts_x, pts_y, is_on, color1, color2, line_color, color3, color4, tol_atol=0.05, tol_y_atol=0.5, point_labels=point_labels, ) trans = ax.get_yaxis_transform() if T_cond_bound is not None: val = T_cond_bound.get("val") label = T_cond_bound.get("label", "Cond_bound") if val is not None and not (isinstance(val, float) and np.isnan(val)): val = float(val) # type: ignore dy = float(T_cond_bound.get("dy", 2.0)) va = T_cond_bound.get("va", "bottom") ax.axhline(y=val, color="oc.red5", linestyle=":", linewidth=dm.lw(0)) offset = dm.make_offset(4, float(dy), cast(mfigure.Figure, ax.figure)) if ax.figure else None transform = trans + offset if offset else trans ax.text( 0.0, val, f"{label}: {val:.1f}°C", color="oc.red5", ha="left", va=va, transform=transform, fontsize=dm.fs(-2), ) if T_evap_bound is not None: val = T_evap_bound.get("val") label = T_evap_bound.get("label", "Evap_bound") if val is not None and not (isinstance(val, float) and np.isnan(val)): val = float(val) # type: ignore dy = float(T_evap_bound.get("dy", -2.0)) va = T_evap_bound.get("va", "top") ax.axhline(y=val, color="oc.orange5", linestyle=":", linewidth=dm.lw(0)) offset = dm.make_offset(4, float(dy), cast(mfigure.Figure, ax.figure)) if ax.figure else None transform = trans + offset if offset else trans ax.text( 0.0, val, f"{label}: {val:.1f}°C", color="oc.orange5", ha="left", va=va, transform=transform, fontsize=dm.fs(-2), ) ax.set_xlabel("Entropy [kJ/(kg·K)]") ax.set_ylabel("Temperature [°C]") ax.set_xlim(limits["xmin"], limits["xmax"]) ax.set_ylim(limits["ymin"], limits["ymax"]) import matplotlib.ticker as ticker ax.xaxis.set_major_locator(ticker.MultipleLocator(0.5)) if refrigerant == "R32": ax.yaxis.set_major_locator(ticker.MultipleLocator(40)) legend_handles = [ ax.plot([], [], color=color1, linewidth=dm.lw(0), label=sat_liquid_label)[0], ax.plot([], [], color=color2, linewidth=dm.lw(0), label=sat_vapor_label)[0], ax.plot( [], [], color=line_color, linewidth=dm.lw(0), marker="o", linestyle=":", markersize=2.5, markerfacecolor=color3, markeredgecolor=color3, markeredgewidth=dm.lw(0), label=ref_cycle_label, )[0], ] ax.legend( handles=legend_handles, loc="upper center", handlelength=1.5, labelspacing=0.5, columnspacing=2, ncol=3, frameon=False, )