=== 컬럼명칭 통일 (c{prefix} → C-{prefix}11) ===
Python 분석스크립트: data pkl 경로 →
gen_temp_profiles: tempref 파일명 →
SteamAdvisorController: TagsFor() 숫자서픽스 → 풀컬럼키(C-6111), ToSuffix() 변환
steam.js: ST_TEMP_COLS ['61',...] → ['C-6111',...], selectbox defaultColumn
appsettings.json: Columns 키 c61/c62/... → C-6111/C-6211/..., DefaultColumn c6111→C-6111
run_column.py: 추출/분석시 col_key = f"C-{{prefix}}11"
C-{x}11_{model,tempref}.json: 신규 명칭 기준 기준프로파일/모델 7컬럼분
=== SteamAdvisor 수정 ===
SteamModel: [JsonPropertyName] 매핑(snake_case → PascalCase 역직렬화)
예외처리: LinearCoeffs.Count < 3 방어코드
steam.js: catch(_) {} → 에러메시지 표시, missing_tags 응답처리
=== Feedforward Controller 개선 ===
ff.js: 상승/하강 양방향 램프 confirm, 방향뱃지(↑↓), Normal 모드 표시
FeedforwardController: 업램프 단독제한 제거(양방향), tcReturnTcTarget/Band 노출
=== DB ===
Hc900DbContext: realtime_table_tagname_key 레거시 UNIQUE 제약/인덱스 DROP 로직
Hc900Controllers: ToDictionaryAsync → GroupBy 변환 (중복 tagname 대응)
=== SIGPIPE 대응 ===
gateway.cpp: signal(SIGPIPE, SIG_IGN) 메인스레드 설치
modbus_tcp.cpp: send() flags 0 → MSG_NOSIGNAL (EPIPE 복구)
sigpipe_ignore.c: LD_PRELOAD 우회 공유라이브러리
Hc900GatewayProcessService: LD_PRELOAD 환경변수 설정
149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
"""
|
|
③ 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 + "C-6111_data.pkl")
|
|
parser.add_argument("--prefix", default="C-6111")
|
|
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()
|