{"spec_id":"psychrometric-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\npsychrometric-basic: Psychrometric Chart for HVAC\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-16\n\"\"\"\n\nimport os\n\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.lines import Line2D\nfrom matplotlib.patches import FancyArrowPatch, Polygon\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first family is brand green; line styles add redundant encoding\nRH_COLOR = \"#009E73\"  # brand green — relative-humidity family (primary structure)\nWB_COLOR = \"#4467A3\"  # blue — wet-bulb (evaporative cooling)\nENTH_COLOR = \"#BD8233\"  # ochre — enthalpy (energy)\nVOL_COLOR = \"#C475FD\"  # lavender — specific volume\nPROC_COLOR = \"#AE3030\"  # matte red — highlighted HVAC process path\n\n# Constants\nP_ATM = 101325.0  # Pa, standard sea-level pressure\nW_MAX = 30.0  # g/kg, top of plotted humidity-ratio range\n\n# Data — psychrometric properties from ASHRAE/Buck formulas (deterministic)\nt_db = np.linspace(-10, 50, 500)\nps_db = 611.21 * np.exp((18.678 - t_db / 234.5) * (t_db / (257.14 + t_db)))\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_xlim(-10, 50)\nax.set_ylim(0, 30)\n\n# Theme-adaptive halo so inline labels stay legible over crossing lines\nhalo = [pe.withStroke(linewidth=2.2, foreground=PAGE_BG)]\nLABEL_FS = 7.5\n\n# Relative-humidity curves (10%–100%); saturation curve (100%) is prominent\nfor rh in np.arange(0.1, 1.001, 0.1):\n    w = 0.621945 * rh * ps_db / (P_ATM - rh * ps_db) * 1000\n    mask = (w >= 0) & (w <= W_MAX)\n    t_p, w_p = t_db[mask], w[mask]\n    if len(t_p) < 3:\n        continue\n    is_sat = abs(rh - 1.0) < 0.01\n    ax.plot(t_p, w_p, color=RH_COLOR, linewidth=2.6 if is_sat else 1.1, alpha=1.0 if is_sat else 0.55)\n    # Label near the upper end of each curve (where the family fans out), but\n    # clamp below 27 g/kg so labels never crowd the title at the top edge\n    i = min(int(len(t_p) * 0.9), len(t_p) - 2)\n    if w_p[i] > 27:\n        below = np.nonzero(w_p <= 27)[0]\n        if len(below):\n            i = min(max(below[-1], 1), len(t_p) - 2)\n    ang = np.degrees(np.arctan2(w_p[i + 1] - w_p[i - 1], t_p[i + 1] - t_p[i - 1]))\n    ax.text(\n        t_p[i] - 0.5,\n        w_p[i] + 0.5,\n        f\"{int(round(rh * 100))}%\",\n        fontsize=LABEL_FS + (1 if is_sat else 0),\n        color=RH_COLOR,\n        fontweight=\"medium\" if is_sat else \"normal\",\n        rotation=ang,\n        rotation_mode=\"anchor\",\n        transform_rotates_text=True,\n        ha=\"right\",\n        va=\"bottom\",\n        path_effects=halo,\n    )\n\n# Wet-bulb temperature lines — labelled at their lower-right ends (clear of RH band)\nfor t_wb in np.arange(0, 35, 5):\n    t_r = np.linspace(t_wb, min(t_wb + 30, 50), 200)\n    ps_wb = 611.21 * np.exp((18.678 - t_wb / 234.5) * (t_wb / (257.14 + t_wb)))\n    ws_wb = 0.621945 * ps_wb / (P_ATM - ps_wb)\n    w = np.maximum(ws_wb - 1.006e3 * (t_r - t_wb) / (2501e3 + 1.86e3 * t_r - 4.186e3 * t_wb), 0) * 1000\n    mask = (w >= 0) & (w <= W_MAX) & (t_r >= -10) & (t_r <= 50)\n    t_p, w_p = t_r[mask], w[mask]\n    if len(t_p) < 3:\n        continue\n    ax.plot(t_p, w_p, color=WB_COLOR, linewidth=1.0, alpha=0.7, linestyle=\"--\")\n    i = len(t_p) - 1\n    ang = np.degrees(np.arctan2(w_p[i] - w_p[i - 1], t_p[i] - t_p[i - 1]))\n    ax.text(\n        t_p[i] + 0.4,\n        w_p[i] + 0.2,\n        f\"{int(t_wb)}°\",\n        fontsize=LABEL_FS,\n        color=WB_COLOR,\n        rotation=ang,\n        rotation_mode=\"anchor\",\n        transform_rotates_text=True,\n        ha=\"left\",\n        va=\"bottom\",\n        path_effects=halo,\n    )\n\n# Enthalpy lines (kJ/kg) — labelled at their upper-left ends\nfor h in np.arange(20, 120, 10):\n    w = (h - 1.006 * t_db) / (2501 + 1.86 * t_db) * 1000\n    mask = (w >= 0) & (w <= W_MAX) & (t_db >= -10) & (t_db <= 50)\n    t_p, w_p = t_db[mask], w[mask]\n    if len(t_p) < 3:\n        continue\n    ax.plot(t_p, w_p, color=ENTH_COLOR, linewidth=1.0, alpha=0.7, linestyle=\"-.\")\n    # Enthalpy lines descend left→right; high-h lines enter at the top edge, so\n    # anchor the label at the first point at/below 26.5 g/kg to clear the title\n    below = np.nonzero(w_p <= 26.5)[0]\n    li = below[0] if len(below) else len(t_p) // 2\n    li = min(max(li, 1), len(t_p) - 2)\n    ang = np.degrees(np.arctan2(w_p[li + 1] - w_p[li - 1], t_p[li + 1] - t_p[li - 1]))\n    ax.text(\n        t_p[li] + 0.3,\n        w_p[li] - 0.1,\n        f\"{int(h)}\",\n        fontsize=LABEL_FS,\n        color=ENTH_COLOR,\n        rotation=ang,\n        rotation_mode=\"anchor\",\n        transform_rotates_text=True,\n        ha=\"left\",\n        va=\"top\",\n        path_effects=halo,\n    )\n\n# Specific-volume lines (m3/kg) — steep; labelled at their lower ends\nfor v in np.arange(0.78, 0.96, 0.02):\n    w = (v * P_ATM / (287.055 * (t_db + 273.15)) - 1) / 1.6078 * 1000\n    mask = (w >= 0) & (w <= W_MAX) & (t_db >= -10) & (t_db <= 50)\n    t_p, w_p = t_db[mask], w[mask]\n    if len(t_p) < 3:\n        continue\n    ax.plot(t_p, w_p, color=VOL_COLOR, linewidth=1.0, alpha=0.65, linestyle=\":\")\n    i = len(t_p) - 1\n    ang = np.degrees(np.arctan2(w_p[i] - w_p[i - 1], t_p[i] - t_p[i - 1]))\n    ax.text(\n        t_p[i] + 0.3,\n        w_p[i] - 0.2,\n        f\"{v:.2f}\",\n        fontsize=LABEL_FS,\n        color=VOL_COLOR,\n        rotation=ang,\n        rotation_mode=\"anchor\",\n        transform_rotates_text=True,\n        ha=\"left\",\n        va=\"top\",\n        path_effects=halo,\n    )\n\n# Comfort zone (~20–26 C, 30–60% RH) — muted fill so it sits behind the data\nct = np.array([20.0, 26.0])\ncps = 611.21 * np.exp((18.678 - ct / 234.5) * (ct / (257.14 + ct)))\ncw_lo = 0.621945 * 0.30 * cps / (P_ATM - 0.30 * cps) * 1000\ncw_hi = 0.621945 * 0.60 * cps / (P_ATM - 0.60 * cps) * 1000\ncomfort = np.array([[20, cw_lo[0]], [26, cw_lo[1]], [26, cw_hi[1]], [20, cw_hi[0]]])\nax.add_patch(\n    Polygon(comfort, closed=True, facecolor=INK_MUTED, alpha=0.16, edgecolor=INK_MUTED, linewidth=1.2, zorder=3)\n)\nax.text(\n    23,\n    comfort[:, 1].mean(),\n    \"Comfort\\nzone\",\n    fontsize=8.5,\n    color=INK_SOFT,\n    ha=\"center\",\n    va=\"center\",\n    fontweight=\"bold\",\n    zorder=4,\n    path_effects=halo,\n)\n\n# HVAC process: hot humid air -> cool & dehumidify -> reheat to comfort\nproc_t = np.array([35.0, 13.0, 24.0])\nproc_rh = np.array([0.50, 0.95, 0.50])\nproc_ps = 611.21 * np.exp((18.678 - proc_t / 234.5) * (proc_t / (257.14 + proc_t)))\nproc_w = 0.621945 * proc_rh * proc_ps / (P_ATM - proc_rh * proc_ps) * 1000\n\narrow_kw = {\"arrowstyle\": \"-|>\", \"color\": PROC_COLOR, \"linewidth\": 2.2, \"mutation_scale\": 14, \"zorder\": 5}\nax.add_patch(FancyArrowPatch((proc_t[0], proc_w[0]), (proc_t[1], proc_w[1]), **arrow_kw))\nax.add_patch(FancyArrowPatch((proc_t[1], proc_w[1]), (proc_t[2], proc_w[2]), **arrow_kw))\nfor t, w, lab in zip(proc_t, proc_w, (\"1\", \"2\", \"3\"), strict=True):\n    ax.plot(t, w, \"o\", color=PROC_COLOR, markersize=6, zorder=6, markeredgecolor=PAGE_BG, markeredgewidth=1.0)\n    ax.text(t + 0.7, w + 0.4, lab, fontsize=9, color=PROC_COLOR, fontweight=\"bold\", zorder=6, path_effects=halo)\nax.text(\n    27,\n    proc_w[0] + 2.2,\n    \"Cool + dehumidify → reheat\",\n    fontsize=8,\n    color=PROC_COLOR,\n    fontstyle=\"italic\",\n    ha=\"center\",\n    zorder=6,\n    path_effects=halo,\n)\n\n# Style\nax.set_xlabel(\"Dry-Bulb Temperature (°C)\", fontsize=11, color=INK)\nax.set_ylabel(\"Humidity Ratio (g/kg dry air)\", fontsize=11, color=INK)\nax.set_title(\"psychrometric-basic · python · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=9, colors=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\nax.grid(True, alpha=0.15, linewidth=0.6, color=INK)\n\nlegend_elements = [\n    Line2D([0], [0], color=RH_COLOR, linewidth=2.2, label=\"Relative humidity\"),\n    Line2D([0], [0], color=WB_COLOR, linewidth=1.6, linestyle=\"--\", label=\"Wet-bulb temp (°C)\"),\n    Line2D([0], [0], color=ENTH_COLOR, linewidth=1.6, linestyle=\"-.\", label=\"Enthalpy (kJ/kg)\"),\n    Line2D([0], [0], color=VOL_COLOR, linewidth=1.6, linestyle=\":\", label=\"Specific volume (m³/kg)\"),\n    Line2D([0], [0], color=PROC_COLOR, linewidth=2.0, marker=\"o\", markersize=5, label=\"HVAC process\"),\n]\nleg = ax.legend(handles=legend_elements, fontsize=8, loc=\"upper left\", framealpha=0.92)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nfor txt in leg.get_texts():\n    txt.set_color(INK_SOFT)\n\n# Save\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}