import math import numpy as np import pandas as pd from typing import Optional def _sat_vapor_pressure_kpa(t_c: float) -> float: return 0.61078 * math.exp((17.2694 * t_c) / (t_c + 237.29)) def pmv_ppd_fanger( ta_c: float, tr_c: Optional[float] = None, rh: float = 50.0, vel: float = 0.1, met: float = 1.2, clo: float = 0.7, wme: float = 0.0, ): if tr_c is None: tr_c = ta_c ta = ta_c tr = tr_c pa_kpa = rh / 100.0 * _sat_vapor_pressure_kpa(ta) pa = pa_kpa * 1000.0 m = met * 58.15 w = wme * 58.15 mw = m - w icl = 0.155 * clo if icl <= 1e-9: icl = 1e-9 if icl <= 0.078: fcl = 1.0 + 1.29 * icl else: fcl = 1.05 + 0.645 * icl hcf = 12.1 * math.sqrt(max(vel, 1e-9)) taa = ta + 273.0 tra = tr + 273.0 tcla = taa + (35.5 - ta) / (3.5 * icl + 0.1) p1 = icl * fcl p2 = p1 * 3.96 p3 = p1 * 100.0 p4 = p1 * taa p5 = 308.7 - 0.028 * mw + p2 * ((tra / 100.0) ** 4) xn = tcla / 100.0 xf = xn eps = 0.00015 n = 0 while True: xf = (xf + xn) / 2.0 tcl = 100.0 * xf - 273.0 hcn = 2.38 * (abs(100.0 * xf - taa) ** 0.25) hc = max(hcf, hcn) xn = (p5 + p4 * hc - p2 * (xf**4)) / (100.0 + p3 * hc) n += 1 if n > 150 or abs(xn - xf) <= eps: break tcl = 100.0 * xn - 273.0 hl1 = 3.05 * 0.001 * (5733.0 - 6.99 * mw - pa) hl2 = 0.42 * (mw - 58.15) if mw > 58.15 else 0.0 hl3 = 1.7 * 0.00001 * m * (5867.0 - pa) hl4 = 0.0014 * m * (34.0 - ta) hl5 = 3.96 * fcl * ((xn**4) - ((tra / 100.0) ** 4)) hl6 = fcl * hc * (tcl - ta) ts = 0.303 * math.exp(-0.036 * m) + 0.028 pmv = ts * (mw - hl1 - hl2 - hl3 - hl4 - hl5 - hl6) ppd = 100.0 - 95.0 * math.exp(-0.03353 * (pmv**4) - 0.2179 * (pmv**2)) return pmv, ppd # ========================================== def ashrae_any(df: pd.DataFrame) -> None: if {"core_ash55_notcomfortable_summer", "core_ash55_notcomfortable_winter"}.issubset(df.columns): # 1. Calculate raw combination raw_val = np.maximum( df["core_ash55_notcomfortable_summer"].astype(float), df["core_ash55_notcomfortable_winter"].astype(float), ) if "core_occ_count" in df.columns: is_occupied = (df["core_occ_count"] > 1e-6).astype(float) df["core_ash55_any_fixed"] = raw_val * is_occupied else: df["core_ash55_any_fixed"] = raw_val else: df["core_ash55_any_fixed"] = np.nan def add_feature_availability_and_registry( df: pd.DataFrame, base_feature_cols, new_feature_cols, ) -> None: for c in base_feature_cols + new_feature_cols: df[f"has_{c}"] = c in df.columns present = [c for c in base_feature_cols + new_feature_cols if c in df.columns] df["feature_registry"] = ";".join(present) def compute_comfort_metrics_inplace( df: pd.DataFrame, location: str, time_step_hours: float, heating_sp: float, cooling_sp: float, zone_temp_keys, zone_occ_keys, rh_keys, ) -> None: missing_t = [k for k in zone_temp_keys if k not in df.columns] missing_o = [k for k in zone_occ_keys if k not in df.columns] if missing_t or missing_o: print(f"[{location}] WARNING: missing temp cols: {missing_t}, occ cols: {missing_o}") df["comfort_violation_degCh"] = 0.0 df["comfort_violation_fixed_degCh"] = 0.0 df["pmv_weighted"] = np.nan df["ppd_weighted"] = np.nan df["rh_weighted"] = np.nan return temps = df[zone_temp_keys].to_numpy(dtype=np.float64) occs = df[zone_occ_keys].to_numpy(dtype=np.float64) total_occ = occs.sum(axis=1) mean_temps = temps.mean(axis=1) comfort_temp = np.where( total_occ > 1e-6, (temps * occs).sum(axis=1) / np.maximum(total_occ, 1e-6), mean_temps, ) if all(k in df.columns for k in rh_keys): rhs = df[rh_keys].to_numpy(dtype=np.float64) rh_weighted = np.where( total_occ > 1e-6, (rhs * occs).sum(axis=1) / np.maximum(total_occ, 1e-6), rhs.mean(axis=1), ) df["rh_weighted"] = rh_weighted else: df["rh_weighted"] = np.nan RH_series = df["rh_weighted"].to_numpy(dtype=np.float64) if "rh_weighted" in df.columns else None VEL = 0.1 MET = 1.2 CLO = 0.7 WME = 0.0 pmv_list = [] ppd_list = [] for i, t in enumerate(comfort_temp): if total_occ[i] <= 1e-6: pmv_list.append(0.0) ppd_list.append(0.0) continue rh_i = float(RH_series[i]) if RH_series is not None and np.isfinite(RH_series[i]) else 50.0 rh_i = float(np.clip(rh_i, 0.0, 100.0)) pmv, ppd = pmv_ppd_fanger( ta_c=float(t), tr_c=float(t), rh=rh_i, vel=VEL, met=MET, clo=CLO, wme=WME, ) pmv_list.append(pmv) ppd_list.append(ppd) df["pmv_weighted"] = np.array(pmv_list, dtype=np.float64) df["ppd_weighted"] = np.array(ppd_list, dtype=np.float64) FIXED_HEAT = 21.0 FIXED_COOL = 24.0 fixed_lower = FIXED_HEAT - 0.5 fixed_upper = FIXED_COOL + 0.5 fixed_dev = np.clip(fixed_lower - comfort_temp, 0.0, None) + np.clip(comfort_temp - fixed_upper, 0.0, None) is_occupied = (total_occ > 1e-6).astype(np.float64) fixed_violation = fixed_dev * time_step_hours * is_occupied df["comfort_violation_degCh"] = fixed_violation df["comfort_violation_fixed_degCh"] = fixed_violation