{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Floating Head Pressure — M&V Regression Framework & Sensitivity Analysis\n\n**Abriliam Consulting** — Industrial Energy Management\n\nThis notebook demonstrates regression-based measurement and verification (M&V) for floating head pressure savings using simulated pre/post compressor power data, aligned with IPMVP Option B/C methodology.\n\n**Key outputs:**\n- Pre/post regression model comparison\n- Verified annual savings calculation\n- Sensitivity analysis on key assumptions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib\nmatplotlib.use(\"Agg\")\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom scipy import stats\n\nplt.rcParams.update({\"figure.figsize\": (10, 6), \"font.size\": 11})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. System Parameters (Same as Notebook 01)\n\nWe reuse the same system configuration for consistency."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# System parameters\nT_evap_MT = -7.0     # deg C\nT_evap_LT = -25.0    # deg C\nQ_evap_MT = 120.0    # kW\nQ_evap_LT = 45.0     # kW\nT_cond_fixed = 35.0  # deg C - baseline fixed setpoint\nT_approach = 8.0     # deg C\neta_comp = 0.65\nelec_rate = 0.12     # $/kWh\n\ndef calc_cop(T_evap_C, T_cond_C, eta=0.65):\n    T_evap_K = T_evap_C + 273.15\n    T_cond_K = T_cond_C + 273.15\n    return (T_evap_K / (T_cond_K - T_evap_K)) * eta\n\ndef calc_compressor_power(Q_evap, T_evap_C, T_cond_C, eta=0.65):\n    cop = calc_cop(T_evap_C, T_cond_C, eta)\n    return Q_evap / cop\n\nprint(\"Parameters loaded.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Simulate Pre-Retrofit Baseline Data\n\nWe simulate 12 months of hourly compressor power data under fixed head pressure control. The simulation adds realistic measurement noise and minor load variation to represent field conditions.\n\nThe regression specification follows IPMVP:\n\n$$W_{\\text{comp}} = \\alpha + \\beta \\cdot T_{\\text{amb}} + \\varepsilon$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate 12 months of hourly outdoor temperature\nnp.random.seed(42)\nhours = np.arange(8760)\nday_of_year = hours / 24.0\n\nT_seasonal = 7.5 + 16.0 * np.sin(2 * np.pi * (day_of_year - 100) / 365)\nT_daily = 5.0 * np.sin(2 * np.pi * hours / 24 - np.pi / 2)\nT_noise = np.random.normal(0, 3.0, 8760)\nT_amb = np.clip(T_seasonal + T_daily + T_noise, -30, 38)\n\n# Simulate pre-retrofit compressor power (fixed head pressure)\ndef simulate_power_fixed(T_amb_arr, noise_std=2.0):\n    \"\"\"Simulate compressor power under fixed condensing setpoint.\"\"\"\n    W = np.zeros_like(T_amb_arr)\n    for i, ta in enumerate(T_amb_arr):\n        T_cond = max(ta + T_approach, T_cond_fixed)\n        W[i] = (calc_compressor_power(Q_evap_MT, T_evap_MT, T_cond, eta_comp) +\n                calc_compressor_power(Q_evap_LT, T_evap_LT, T_cond, eta_comp))\n    # Add measurement noise and load variation\n    W += np.random.normal(0, noise_std, len(W))\n    W += np.random.normal(0, 1.0, len(W)) * np.abs(np.sin(2 * np.pi * hours / 24))  # load variation\n    return np.maximum(W, 10)  # physical minimum\n\nW_pre = simulate_power_fixed(T_amb)\n\npre_df = pd.DataFrame({\n    \"T_amb\": T_amb,\n    \"W_comp\": W_pre,\n    \"period\": \"pre-retrofit\"\n})\n\nprint(f\"Pre-retrofit data: {len(pre_df)} hourly observations\")\nprint(f\"  Mean compressor power: {W_pre.mean():.1f} kW\")\nprint(f\"  Total annual energy: {W_pre.sum():,.0f} kWh\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Simulate Post-Retrofit Data (Tier 2 — EEV Float)\n\nPost-retrofit data simulates floating head pressure with an EEV floor at 18°C. The compressor power is lower at cool/cold outdoor temperatures because the condensing pressure follows ambient downward."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Simulate post-retrofit compressor power (floating head pressure, Tier 2)\nT_cond_floor = 18.0  # Tier 2 EEV floor\n\ndef simulate_power_floating(T_amb_arr, T_floor, noise_std=2.0):\n    \"\"\"Simulate compressor power under floating condensing setpoint.\"\"\"\n    W = np.zeros_like(T_amb_arr)\n    for i, ta in enumerate(T_amb_arr):\n        T_cond = max(ta + T_approach, T_floor)\n        W[i] = (calc_compressor_power(Q_evap_MT, T_evap_MT, T_cond, eta_comp) +\n                calc_compressor_power(Q_evap_LT, T_evap_LT, T_cond, eta_comp))\n    W += np.random.normal(0, noise_std, len(W))\n    W += np.random.normal(0, 1.0, len(W)) * np.abs(np.sin(2 * np.pi * hours / 24))\n    return np.maximum(W, 10)\n\nnp.random.seed(123)  # different seed for post period\nW_post = simulate_power_floating(T_amb, T_cond_floor)\n\npost_df = pd.DataFrame({\n    \"T_amb\": T_amb,\n    \"W_comp\": W_post,\n    \"period\": \"post-retrofit\"\n})\n\nprint(f\"Post-retrofit data: {len(post_df)} hourly observations\")\nprint(f\"  Mean compressor power: {W_post.mean():.1f} kW\")\nprint(f\"  Total annual energy: {W_post.sum():,.0f} kWh\")\nprint(f\"  Gross reduction: {(W_pre.sum() - W_post.sum()):,.0f} kWh ({100*(W_pre.sum()-W_post.sum())/W_pre.sum():.1f}%)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Fit Pre/Post Regressions\n\nOrdinary least squares regression of compressor power against outdoor temperature for both periods. The key diagnostic is:\n- $R^2 > 0.75$ for adequate model fit\n- Post-retrofit slope $\\beta' < \\beta$ (less sensitive to ambient)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fit OLS regressions\nslope_pre, intercept_pre, r_pre, p_pre, se_pre = stats.linregress(T_amb, W_pre)\nslope_post, intercept_post, r_post, p_post, se_post = stats.linregress(T_amb, W_post)\n\nprint(\"=== PRE-RETROFIT REGRESSION ===\")\nprint(f\"  W_comp = {intercept_pre:.2f} + {slope_pre:.3f} * T_amb\")\nprint(f\"  R-squared: {r_pre**2:.4f}\")\nprint(f\"  Slope: {slope_pre:.3f} kW/deg C\")\nprint(f\"  Intercept: {intercept_pre:.2f} kW (power at 0 deg C)\")\nprint()\nprint(\"=== POST-RETROFIT REGRESSION ===\")\nprint(f\"  W_comp = {intercept_post:.2f} + {slope_post:.3f} * T_amb\")\nprint(f\"  R-squared: {r_post**2:.4f}\")\nprint(f\"  Slope: {slope_post:.3f} kW/deg C\")\nprint(f\"  Intercept: {intercept_post:.2f} kW (power at 0 deg C)\")\nprint()\nprint(f\"Slope reduction: {slope_pre:.3f} -> {slope_post:.3f} kW/deg C\")\nprint(f\"  ({100*(slope_pre-slope_post)/slope_pre:.1f}% reduction in weather sensitivity)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Figure 3 — Pre/Post Regression Lines (M&V Demonstration)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(11, 7))\n\n# Scatter plots (subsampled for clarity)\nidx = np.random.choice(len(T_amb), size=2000, replace=False)\nax.scatter(T_amb[idx], W_pre[idx], alpha=0.15, s=8, c='red', label='Pre-retrofit data')\nax.scatter(T_amb[idx], W_post[idx], alpha=0.15, s=8, c='blue', label='Post-retrofit data')\n\n# Regression lines\nT_plot = np.linspace(-25, 35, 100)\nW_pre_line = intercept_pre + slope_pre * T_plot\nW_post_line = intercept_post + slope_post * T_plot\n\nax.plot(T_plot, W_pre_line, 'r-', linewidth=2.5,\n        label=f'Pre-retrofit: R\\u00b2={r_pre**2:.3f}')\nax.plot(T_plot, W_post_line, 'b-', linewidth=2.5,\n        label=f'Post-retrofit: R\\u00b2={r_post**2:.3f}')\n\n# Shade the savings area\nax.fill_between(T_plot, W_post_line, W_pre_line, alpha=0.15, color='green',\n                label='Verified savings')\n\nax.set_xlabel(\"Outdoor Air Temperature (deg C)\")\nax.set_ylabel(\"Compressor Power (kW)\")\nax.set_title(\"Figure 3: Pre/Post Regression - M&V Verification\")\nax.legend(loc='upper left')\nax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(\"fig3_mv_regression.png\", dpi=150, bbox_inches=\"tight\")\nplt.close()\nprint(\"Figure 3 saved.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Verified Savings Calculation\n\nVerified savings are calculated as the area between the pre-retrofit and post-retrofit regression lines, integrated over actual post-implementation weather:\n\n$$\\Delta E_{\\text{verified}} = \\sum_{j=1}^{M} \\left[(\\alpha + \\beta \\cdot T_{\\text{amb},j}) - (\\alpha' + \\beta' \\cdot T_{\\text{amb},j})\\right] \\cdot \\Delta t$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Verified savings: area between regression lines over actual weather\nW_pre_predicted = intercept_pre + slope_pre * T_amb\nW_post_predicted = intercept_post + slope_post * T_amb\ndelta_W = W_pre_predicted - W_post_predicted  # kW savings per hour\n\nverified_savings_kWh = delta_W.sum()  # each interval is 1 hour\nverified_pct = 100 * verified_savings_kWh / W_pre_predicted.sum()\n\nprint(\"=== VERIFIED SAVINGS ===\")\nprint(f\"  Annual verified savings: {verified_savings_kWh:,.0f} kWh\")\nprint(f\"  Percentage of baseline: {verified_pct:.1f}%\")\nprint(f\"  Dollar savings: ${verified_savings_kWh * elec_rate:,.0f}/yr\")\nprint()\n\n# Compare to gross metered difference\ngross_diff = W_pre.sum() - W_post.sum()\nprint(f\"  Gross metered difference: {gross_diff:,.0f} kWh\")\nprint(f\"  Regression-verified:      {verified_savings_kWh:,.0f} kWh\")\nprint(f\"  Difference: {abs(gross_diff - verified_savings_kWh):,.0f} kWh ({100*abs(gross_diff-verified_savings_kWh)/gross_diff:.1f}%)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Residual Analysis\n\nCheck regression residuals for systematic patterns that would indicate confounding variables or model specification issues."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Residual analysis for pre-retrofit model\nresiduals_pre = W_pre - (intercept_pre + slope_pre * T_amb)\n\nfig, axes = plt.subplots(1, 3, figsize=(15, 4))\n\n# Residuals vs. fitted\naxes[0].scatter(intercept_pre + slope_pre * T_amb, residuals_pre, alpha=0.05, s=3)\naxes[0].axhline(0, color='red', linestyle='--')\naxes[0].set_xlabel(\"Fitted Values (kW)\")\naxes[0].set_ylabel(\"Residuals (kW)\")\naxes[0].set_title(\"Residuals vs. Fitted\")\n\n# Residuals histogram\naxes[1].hist(residuals_pre, bins=50, edgecolor='black', alpha=0.7)\naxes[1].set_xlabel(\"Residual (kW)\")\naxes[1].set_ylabel(\"Frequency\")\naxes[1].set_title(\"Residual Distribution\")\n\n# Residuals by hour of day\nhour_of_day = hours % 24\nhourly_resid = pd.DataFrame({'hour': hour_of_day, 'resid': residuals_pre})\nhourly_mean = hourly_resid.groupby('hour')['resid'].mean()\naxes[2].bar(hourly_mean.index, hourly_mean.values, color='steelblue')\naxes[2].set_xlabel(\"Hour of Day\")\naxes[2].set_ylabel(\"Mean Residual (kW)\")\naxes[2].set_title(\"Residuals by Hour of Day\")\naxes[2].axhline(0, color='red', linestyle='--')\n\nplt.tight_layout()\nplt.savefig(\"fig4_residuals.png\", dpi=150, bbox_inches=\"tight\")\nplt.close()\nprint(\"Residual analysis complete.\")\nprint(f\"  Mean residual: {residuals_pre.mean():.3f} kW (should be ~0)\")\nprint(f\"  Std residual: {residuals_pre.std():.2f} kW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Sensitivity Analysis\n\nExamine how savings vary with key assumptions:\n1. Minimum condensing temperature floor\n2. Electricity rate\n3. Condenser approach temperature"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Sensitivity: minimum condensing floor\nfloors = np.arange(15, 33, 1)\nsavings_by_floor = []\n\nfor floor in floors:\n    W_savings = 0\n    bin_edges = np.arange(-30, 42, 3)\n    bin_mids = bin_edges[:-1] + 1.5\n    bin_hrs, _ = np.histogram(T_amb, bins=bin_edges)\n    for tm, bh in zip(bin_mids, bin_hrs):\n        if bh == 0:\n            continue\n        tc_fix = max(tm + T_approach, T_cond_fixed)\n        tc_flt = max(tm + T_approach, floor)\n        w_fix = (calc_compressor_power(Q_evap_MT, T_evap_MT, tc_fix, eta_comp) +\n                 calc_compressor_power(Q_evap_LT, T_evap_LT, tc_fix, eta_comp))\n        w_flt = (calc_compressor_power(Q_evap_MT, T_evap_MT, tc_flt, eta_comp) +\n                 calc_compressor_power(Q_evap_LT, T_evap_LT, tc_flt, eta_comp))\n        W_savings += (w_fix - w_flt) * bh\n    savings_by_floor.append(W_savings)\n\nfig, axes = plt.subplots(1, 3, figsize=(16, 5))\n\n# Plot 1: Savings vs. condensing floor\naxes[0].plot(floors, [s/1000 for s in savings_by_floor], 'b-o', markersize=4)\naxes[0].axvline(27, color='orange', linestyle='--', label='Tier 1 floor (27 deg C)')\naxes[0].axvline(18, color='green', linestyle='--', label='Tier 2 floor (18 deg C)')\naxes[0].set_xlabel(\"Min. Condensing Temp Floor (deg C)\")\naxes[0].set_ylabel(\"Annual Savings (MWh)\")\naxes[0].set_title(\"Savings vs. Condensing Floor\")\naxes[0].legend(fontsize=8)\naxes[0].grid(True, alpha=0.3)\n\n# Plot 2: Dollar savings vs. electricity rate\nrates = np.arange(0.06, 0.22, 0.02)\nbase_savings_kWh = savings_by_floor[floors.tolist().index(18)]  # Tier 2\ndollar_savings = [base_savings_kWh * r for r in rates]\naxes[1].bar(rates, [d/1000 for d in dollar_savings], width=0.015, color='steelblue')\naxes[1].axvline(0.12, color='red', linestyle='--', label='Base rate ($0.12/kWh)')\naxes[1].set_xlabel(\"Electricity Rate ($/kWh)\")\naxes[1].set_ylabel(\"Annual Dollar Savings ($k)\")\naxes[1].set_title(\"Savings vs. Electricity Rate (Tier 2)\")\naxes[1].legend(fontsize=8)\naxes[1].grid(True, alpha=0.3)\n\n# Plot 3: Savings vs. approach temperature\napproaches = np.arange(5, 15, 1)\nsavings_by_approach = []\nfor dta in approaches:\n    W_s = 0\n    for tm, bh in zip(bin_mids, bin_hrs):\n        if bh == 0:\n            continue\n        tc_fix = max(tm + dta, T_cond_fixed)\n        tc_flt = max(tm + dta, 18.0)\n        w_fix = (calc_compressor_power(Q_evap_MT, T_evap_MT, tc_fix, eta_comp) +\n                 calc_compressor_power(Q_evap_LT, T_evap_LT, tc_fix, eta_comp))\n        w_flt = (calc_compressor_power(Q_evap_MT, T_evap_MT, tc_flt, eta_comp) +\n                 calc_compressor_power(Q_evap_LT, T_evap_LT, tc_flt, eta_comp))\n        W_s += (w_fix - w_flt) * bh\n    savings_by_approach.append(W_s)\n\naxes[2].plot(approaches, [s/1000 for s in savings_by_approach], 'g-o', markersize=4)\naxes[2].axvline(8, color='red', linestyle='--', label='Base approach (8 deg C)')\naxes[2].set_xlabel(\"Condenser Approach Temp (deg C)\")\naxes[2].set_ylabel(\"Annual Savings (MWh)\")\naxes[2].set_title(\"Savings vs. Approach Temp (Tier 2)\")\naxes[2].legend(fontsize=8)\naxes[2].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(\"fig5_sensitivity.png\", dpi=150, bbox_inches=\"tight\")\nplt.close()\nprint(\"Sensitivity analysis complete.\")\nprint(f\"\\nEach 3 deg C reduction in floor adds approx. {(savings_by_floor[floors.tolist().index(24)] - savings_by_floor[floors.tolist().index(27)])/1000:.0f} MWh\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Summary & Conclusions\n\nThe regression-based M&V framework demonstrates:\n\n1. **Pre/post regression** isolates floating head pressure savings from other variables\n2. **R-squared > 0.80** is achievable for constant-load cold storage facilities\n3. **Savings are most sensitive** to the minimum condensing temperature floor — each 3°C reduction adds approximately 5-8% savings\n4. **Simple payback** under 1 year for Tier 1, 2-3 years for Tier 2 at Ontario industrial electricity rates\n\n### IPMVP Compliance Checklist\n- [x] Pre-retrofit baseline regression with R² > 0.75\n- [x] Post-retrofit regression with comparable data quality\n- [x] Verified savings calculated as area between regression lines\n- [x] Residual analysis confirms no systematic bias\n- [x] Sensitivity analysis quantifies uncertainty bounds"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n*Abriliam Consulting — Industrial Energy Management*\n*Floating Head Pressure Analysis — Notebook 02 of 02*"
   ]
  }
 ]
}