""" ③ SHUTDOWN 절차 학습 (few-shot). startup의 역순. 형제 컬럼 호환: --data, --prefix CLI 인자. """ import argparse import numpy as np import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/" def detect_cutoffs(df): """★제품 컷오프★ 이벤트: product >100→<50 하강엣지이고 직후 steam도 하강(shutdown).""" prod = df["product"].values steam_op = df["steam_op"].values reb = df["reb_temp"].values outs = [] i = 60 n = len(df) while i < n: if prod[i] < 50 and prod[i-1] >= 100 and reb[i] > 60: fwd = steam_op[i:min(n, i+60)] if np.nanmean(fwd) < np.nanmean(steam_op[max(0, i-60):i]) * 0.8: outs.append(i) i += 720 continue i += 1 return outs def shutdown_milestones(df, co): """컷오프 인덱스 co 기준 역방향 절차 추출.""" tc = df["dtat"].iloc[co] n = len(df) def mins(i): return None if i is None else (df["dtat"].iloc[i] - tc).total_seconds() / 60 feed_start = None feed_vals = df["feed"].values for j in range(co, max(0, co - 1200), -1): if feed_vals[j] < 100: feed_start = j if feed_start is not None and j > 0: if feed_vals[j] > feed_vals[min(j + 30, co)] * 0.85: continue if feed_vals[j] > 250 and feed_vals[j] > feed_vals[min(j + 1, co)] * 0.98: feed_start = j break steam_off = None for j in range(co, min(n, co + 600)): if df["steam_op"].iloc[j] < 5: steam_off = j break vacuum_off = None for j in range(co, min(n, co + 1200)): if df["vacuum"].iloc[j] > 300: vacuum_off = j break prod_off = None for j in range(co, min(n, co + 120)): if df["product"].iloc[j] < 10: prod_off = j break cold = None for j in range(co, min(n, co + 2400)): if df["reb_temp"].iloc[j] < 40: cold = j break r = df.iloc[co] return dict(cutoff_time=tc, feed_to_cutoff=-(mins(feed_start)) if feed_start is not None else None, cutoff_to_steam_off=mins(steam_off) if steam_off else None, cutoff_to_vacuum_off=mins(vacuum_off) if vacuum_off else None, cutoff_to_prod_off=mins(prod_off) if prod_off else None, cutoff_to_cold=mins(cold) if cold else None, cutoff_rebA=r["reb_temp"], cutoff_TC=r["T_C"], cutoff_TD=r["T_D"], cutoff_dT_AD=r["reb_temp"] - r["T_D"]) def main(): parser = argparse.ArgumentParser() parser.add_argument("--data", default=BASE + "c6111_data.pkl") parser.add_argument("--prefix", default="c6111") args = parser.parse_args() df = pd.read_pickle(args.data).sort_values("dtat").reset_index(drop=True) cutoffs = detect_cutoffs(df) print(f"탐지된 ★제품 컷오프★(shutdown 진입) 이벤트: {len(cutoffs)}개") if not cutoffs: print(" [skip] shutdown 이벤트 없음 — 플롯 생략") return rows, windows = [], [] for co in cutoffs: w = df.iloc[max(0, co - 360):min(len(df), co + 360)].copy() w["rel_min"] = (w["dtat"] - df["dtat"].iloc[co]).dt.total_seconds() / 60 windows.append(w) rows.append(shutdown_milestones(df, co)) M = pd.DataFrame(rows) pd.set_option("display.width", 220) print("\n=== 제품컷오프 기준 절차(분) + 셧다운 시점 컬럼상태 ===") cols = ["cutoff_time", "feed_to_cutoff", "cutoff_to_steam_off", "cutoff_to_vacuum_off", "cutoff_to_prod_off", "cutoff_to_cold", "cutoff_rebA", "cutoff_TC", "cutoff_dT_AD"] show = M[cols].copy() show["cutoff_time"] = show["cutoff_time"].dt.strftime("%m-%d %H:%M") print(show.round(1).to_string(index=False)) print("\n=== 셧다운 레시피(중앙값) ===") print(f" 피드감소→컷오프: {M.feed_to_cutoff.median():.0f}분") print(f" 컷오프→스팀차단 : {M.cutoff_to_steam_off.median():.0f}분") print(f" 컷오프→진공해제 : {M.cutoff_to_vacuum_off.median():.0f}분") print(f" 컷오프→제품0 : {M.cutoff_to_prod_off.median():.0f}분") print(f" 컷오프→냉각 : {M.cutoff_to_cold.median():.0f}분") reb_std = M.cutoff_rebA.std() if len(M) > 1 else 0.0 tc_std = M.cutoff_TC.std() if len(M) > 1 else 0.0 print(f" ★셧다운 트리거: reb-A={M.cutoff_rebA.median():.1f}±{reb_std:.1f}℃, " f"T_C={M.cutoff_TC.median():.1f}±{tc_std:.2f}℃, ΔT(A-D)={M.cutoff_dT_AD.median():.1f}℃") fig, ax = plt.subplots(4, 1, figsize=(13, 11), sharex=True) for k, w in enumerate(windows): c = plt.cm.tab10(k) ax[0].plot(w.rel_min, w.reb_temp, color=c, lw=.9, label=f"sh{k+1} {w.dtat.iloc[len(w)//2]:%m-%d}") ax[0].plot(w.rel_min, w["T_D"], color=c, lw=.6, ls=":") ax[1].plot(w.rel_min, w.steam_flow, color=c, lw=.9) ax[2].plot(w.rel_min, w.reflux, color=c, lw=.9) ax[2].plot(w.rel_min, w["product"], color=c, lw=.9, ls="--") ax[3].plot(w.rel_min, w.feed, color=c, lw=.9) ax[0].set_ylabel("reb_temp/T_D(:)"); ax[0].legend(fontsize=7) ax[0].set_title("SHUTDOWN aligned at PRODUCT CUT-OFF (rel=0)") ax[1].set_ylabel("steam flow"); ax[2].set_ylabel("reflux/product(--)") ax[3].set_ylabel("feed"); ax[3].set_xlabel("minutes from product cut-off") for a in ax: a.axvline(0, c="k", lw=.5) fig.tight_layout(); fig.savefig(BASE + f"{args.prefix}_shutdown.png", dpi=95) print(f"\n플롯 저장: {BASE}{args.prefix}_shutdown.png") if __name__ == "__main__": main()