{"spec_id":"psychrometric-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\npsychrometric-basic: Psychrometric Chart for HVAC\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-16\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\n\n\n# Theme-adaptive chrome (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\"\n\n# Imprint palette — first family is brand green, then canonical order; line styles\n# reinforce colour for colourblind safety.\nRH_COLOR = \"#009E73\"  # brand green — relative-humidity curves (most prominent family)\nWB_COLOR = \"#4467A3\"  # blue — wet-bulb isotherms\nENTH_COLOR = \"#AE3030\"  # matte red — constant enthalpy (energy)\nSV_COLOR = \"#BD8233\"  # ochre — constant specific volume\nCOMFORT_COLOR = \"#2ABCCD\"  # cyan — comfort-zone region\nPROCESS_COLOR = \"#C475FD\"  # lavender — HVAC process path\n\n# Data — moist-air properties at standard sea-level pressure (101.325 kPa)\nnp.random.seed(42)\nP_ATM = 101325  # Pa\nT_GRID = np.linspace(-10, 50, 600)\n\n# Saturation vapour pressure (Pa) and saturation humidity ratio (g/kg) over water\n# (ASHRAE) — a single continuous relation across the whole range keeps the curves\n# smooth (no kink at 0 °C).\nP_SAT = np.exp(23.196 - 3816.44 / (T_GRID + 227.02))\nW_SAT = 0.62198 * P_SAT / (P_ATM - P_SAT) * 1000\n\n# Constant relative-humidity curves (10% – 100%)\nrh_rows = []\nfor rh in np.arange(10, 101, 10):\n    p_v = (rh / 100) * P_SAT\n    w = 0.62198 * p_v / (P_ATM - p_v) * 1000\n    keep = (w >= 0) & (w <= 30)\n    for t, wv in zip(T_GRID[keep], w[keep], strict=True):\n        rh_rows.append({\"t\": t, \"w\": wv, \"rh\": f\"{rh}%\"})\nrh_df = pd.DataFrame(rh_rows)\n\n# Constant wet-bulb isotherms (line slope ≈ -0.402 g/kg per °C)\nwb_rows = []\nfor t_wb in np.arange(2, 31, 4):\n    w0 = float(np.interp(t_wb, T_GRID, W_SAT))\n    t_line = np.linspace(t_wb, 50, 160)\n    w_line = w0 - 0.402 * (t_line - t_wb)\n    cap = np.minimum(np.interp(t_line, T_GRID, W_SAT), 30)\n    keep = (w_line >= 0) & (w_line <= cap)\n    for t, wv in zip(t_line[keep], w_line[keep], strict=True):\n        wb_rows.append({\"t\": t, \"w\": wv, \"twb\": f\"{t_wb}\"})\nwb_df = pd.DataFrame(wb_rows)\n\n# Constant enthalpy lines (kJ/kg dry air)\nenth_rows = []\nfor h in np.arange(20, 101, 20):\n    w_line = (h - 1.006 * T_GRID) / (2.501 + 0.00186 * T_GRID)\n    keep = (w_line >= 0) & (w_line <= np.minimum(W_SAT, 30))\n    for t, wv in zip(T_GRID[keep], w_line[keep], strict=True):\n        enth_rows.append({\"t\": t, \"w\": wv, \"h\": f\"{h}\"})\nenth_df = pd.DataFrame(enth_rows)\n\n# Constant specific-volume lines (m³/kg dry air)\nsv_rows = []\nfor v in np.arange(0.80, 0.93, 0.04):\n    w_line = ((v * P_ATM / 1000) / (0.287042 * (T_GRID + 273.15)) - 1) / 1.6078 * 1000\n    keep = (w_line >= 0) & (w_line <= np.minimum(W_SAT, 30))\n    for t, wv in zip(T_GRID[keep], w_line[keep], strict=True):\n        sv_rows.append({\"t\": t, \"w\": wv, \"v\": f\"{v:.2f}\"})\nsv_df = pd.DataFrame(sv_rows)\n\n# Comfort zone (≈20–26 °C, 30–60% RH) and a cooling + dehumidification process path\ncomfort_t = np.array([20, 26, 26, 20])\ncomfort_w = np.array(\n    [\n        0.62198 * (0.30 * np.interp(20, T_GRID, P_SAT)) / (P_ATM - 0.30 * np.interp(20, T_GRID, P_SAT)) * 1000,\n        0.62198 * (0.30 * np.interp(26, T_GRID, P_SAT)) / (P_ATM - 0.30 * np.interp(26, T_GRID, P_SAT)) * 1000,\n        0.62198 * (0.60 * np.interp(26, T_GRID, P_SAT)) / (P_ATM - 0.60 * np.interp(26, T_GRID, P_SAT)) * 1000,\n        0.62198 * (0.60 * np.interp(20, T_GRID, P_SAT)) / (P_ATM - 0.60 * np.interp(20, T_GRID, P_SAT)) * 1000,\n    ]\n)\n\nstate_points = pd.DataFrame(\n    {\n        \"t\": [34.0, 13.0, 24.0],\n        \"w\": [\n            0.62198 * (0.45 * np.interp(34, T_GRID, P_SAT)) / (P_ATM - 0.45 * np.interp(34, T_GRID, P_SAT)) * 1000,\n            float(np.interp(13, T_GRID, W_SAT)),\n            0.62198 * (0.50 * np.interp(24, T_GRID, P_SAT)) / (P_ATM - 0.50 * np.interp(24, T_GRID, P_SAT)) * 1000,\n        ],\n        \"label\": [\"A · supply 34°C/45%\", \"B · cooled 13°C/100%\", \"C · room 24°C/50%\"],\n    }\n)\n\n# Plot — 16:9 canvas (8 × 4.5 in @ dpi=400 → 3200 × 1800 px)\nsns.set_theme(\n    style=\"whitegrid\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.12,\n        \"grid.linewidth\": 0.6,\n        \"font.family\": \"sans-serif\",\n    },\n)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\n\n# Comfort zone sits behind the property lines\nax.fill(comfort_t, comfort_w, color=COMFORT_COLOR, alpha=0.16, zorder=1)\nax.plot(\n    np.append(comfort_t, comfort_t[0]),\n    np.append(comfort_w, comfort_w[0]),\n    color=COMFORT_COLOR,\n    linewidth=1.2,\n    alpha=0.7,\n    zorder=1,\n)\n\n# Property-line families, each drawn as one seaborn lineplot (units → one line per level)\nsns.lineplot(\n    data=sv_df,\n    x=\"t\",\n    y=\"w\",\n    units=\"v\",\n    estimator=None,\n    color=SV_COLOR,\n    linewidth=0.8,\n    linestyle=\":\",\n    alpha=0.7,\n    ax=ax,\n    legend=False,\n)\nsns.lineplot(\n    data=enth_df,\n    x=\"t\",\n    y=\"w\",\n    units=\"h\",\n    estimator=None,\n    color=ENTH_COLOR,\n    linewidth=0.9,\n    linestyle=\"-.\",\n    alpha=0.65,\n    ax=ax,\n    legend=False,\n)\nsns.lineplot(\n    data=wb_df,\n    x=\"t\",\n    y=\"w\",\n    units=\"twb\",\n    estimator=None,\n    color=WB_COLOR,\n    linewidth=0.9,\n    linestyle=\"--\",\n    alpha=0.7,\n    ax=ax,\n    legend=False,\n)\nsns.lineplot(\n    data=rh_df, x=\"t\", y=\"w\", units=\"rh\", estimator=None, color=RH_COLOR, linewidth=1.2, alpha=0.85, ax=ax, legend=False\n)\n\n# Saturation curve (100% RH) — prominent upper boundary\nsat = rh_df[rh_df[\"rh\"] == \"100%\"]\nax.plot(sat[\"t\"], sat[\"w\"], color=RH_COLOR, linewidth=2.6, zorder=4)\n\n# HVAC process path: cooling + dehumidification (A→B), then sensible reheat (B→C)\nfor i in range(2):\n    ax.annotate(\n        \"\",\n        xy=(state_points[\"t\"][i + 1], state_points[\"w\"][i + 1]),\n        xytext=(state_points[\"t\"][i], state_points[\"w\"][i]),\n        arrowprops={\"arrowstyle\": \"-|>\", \"color\": PROCESS_COLOR, \"lw\": 2.6},\n        zorder=5,\n    )\nsns.scatterplot(\n    data=state_points,\n    x=\"t\",\n    y=\"w\",\n    color=PROCESS_COLOR,\n    s=130,\n    edgecolor=PAGE_BG,\n    linewidth=1.6,\n    zorder=6,\n    legend=False,\n    ax=ax,\n)\n\n# Direct labels (spec: label property lines on the chart) — spread to edges to avoid crowding\nfor rh in rh_df[\"rh\"].unique():\n    seg = rh_df[rh_df[\"rh\"] == rh]\n    ax.text(\n        float(seg[\"t\"].iloc[-1]) + 0.3,\n        float(seg[\"w\"].iloc[-1]),\n        rh,\n        fontsize=7,\n        color=RH_COLOR,\n        ha=\"left\",\n        va=\"center\",\n        fontweight=\"bold\",\n    )\nfor twb in wb_df[\"twb\"].unique():\n    seg = wb_df[wb_df[\"twb\"] == twb]\n    ax.text(\n        float(seg[\"t\"].iloc[-1]) + 0.3,\n        float(seg[\"w\"].iloc[-1]) - 0.2,\n        f\"{twb}°\",\n        fontsize=6.5,\n        color=WB_COLOR,\n        ha=\"left\",\n        va=\"top\",\n    )\nfor h in enth_df[\"h\"].unique():\n    seg = enth_df[enth_df[\"h\"] == h]\n    ax.text(\n        float(seg[\"t\"].iloc[0]) - 0.3,\n        float(seg[\"w\"].iloc[0]) + 0.2,\n        h,\n        fontsize=6.5,\n        color=ENTH_COLOR,\n        ha=\"right\",\n        va=\"bottom\",\n        rotation=-38,\n    )\nfor v in sv_df[\"v\"].unique():\n    seg = sv_df[sv_df[\"v\"] == v]\n    ax.text(\n        float(seg[\"t\"].iloc[0]) + 0.2,\n        float(seg[\"w\"].iloc[0]) - 0.2,\n        v,\n        fontsize=6.5,\n        color=SV_COLOR,\n        ha=\"left\",\n        va=\"top\",\n    )\n\nax.text(\n    23,\n    float(comfort_w.mean()),\n    \"Comfort\\nzone\",\n    fontsize=8,\n    color=COMFORT_COLOR,\n    ha=\"center\",\n    va=\"center\",\n    fontweight=\"bold\",\n)\nfor _, row in state_points.iterrows():\n    dx = -0.8 if row[\"label\"].startswith(\"B\") else 0.8\n    ha = \"right\" if row[\"label\"].startswith(\"B\") else \"left\"\n    ax.text(\n        row[\"t\"] + dx,\n        row[\"w\"] + 0.7,\n        row[\"label\"],\n        fontsize=7.5,\n        color=PROCESS_COLOR,\n        ha=ha,\n        va=\"bottom\",\n        fontweight=\"bold\",\n    )\n\n# Legend — sits in the empty upper-left zone (cold + humid is physically unreachable)\nlegend_handles = [\n    Line2D([0], [0], color=RH_COLOR, lw=2.2, label=\"Relative humidity\"),\n    Line2D([0], [0], color=WB_COLOR, lw=1.4, ls=\"--\", label=\"Wet-bulb temp (°C)\"),\n    Line2D([0], [0], color=ENTH_COLOR, lw=1.4, ls=\"-.\", label=\"Enthalpy (kJ/kg)\"),\n    Line2D([0], [0], color=SV_COLOR, lw=1.4, ls=\":\", label=\"Specific volume (m³/kg)\"),\n    Line2D(\n        [0],\n        [0],\n        color=PROCESS_COLOR,\n        lw=2.4,\n        marker=\"o\",\n        markersize=6,\n        markeredgecolor=PAGE_BG,\n        label=\"HVAC process path\",\n    ),\n]\nlegend = ax.legend(\n    handles=legend_handles, loc=\"upper left\", fontsize=8, framealpha=0.95, facecolor=ELEVATED_BG, edgecolor=INK_SOFT\n)\nlegend.get_title().set_color(INK)\nfor text in legend.get_texts():\n    text.set_color(INK_SOFT)\n\n# Style\nax.set_xlim(-10, 50)\nax.set_ylim(0, 30)\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 · seaborn · anyplot.ai\", fontsize=13, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=9, colors=INK_SOFT)\nsns.despine(ax=ax)\n\n# Save\nfig.subplots_adjust(left=0.07, right=0.97, top=0.93, bottom=0.1)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}