Files
HC900-Crawler/scripts/analysis/c6111_startup.py
windpacer 7409fabc58 컬럼명칭 통일 C-xxxxx + SIGPIPE 대응 + SteamAdvisor/FF 개선
=== 컬럼명칭 통일 (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 환경변수 설정
2026-06-07 00:29:47 +09:00

112 lines
4.7 KiB
Python

"""
② START-UP 절차 학습 (few-shot).
형제 컬럼 호환: --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_cutins(df):
"""★제품 컷인★ 이벤트: product 0→>100 상향, 직전 30분 라인아웃(product<50)이고 hot(reb>75)."""
prod = df["product"].values
reb = df["reb_temp"].values
outs = []
i = 60
n = len(df)
while i < n:
if prod[i] > 100 and prod[i-1] <= 100:
pre = prod[max(0, i-60):i] # 직전 30분
if np.nanmedian(pre) < 50 and reb[i] > 75: # 라인아웃(제품off)+hot
outs.append(i)
i += 720
continue
i += 1
return outs
def milestones(df, ci):
"""제품 컷인 인덱스 ci 기준 절차 추출."""
tc = df["dtat"].iloc[ci]
# 역방향: 스팀투입(steam_op>10 연속 시작) — 컷인 직전 steam off→on
back = df.iloc[max(0, ci-1200):ci]
off = back[back["steam_op"] <= 10]
i_steam = off.index[-1] + 1 if len(off) else back.index[0]
# 리플럭스 확립(스팀투입 이후 reflux>100 첫)
aft = df.iloc[i_steam:ci]
r_on = aft[aft["reflux"] > 100]
i_refl = r_on.index[0] if len(r_on) else None
# 풀로드(컷인 이후 feed>250 첫)
fwd = df.iloc[ci:ci+1200]
f_on = fwd[fwd["feed"] > 250]
i_full = f_on.index[0] if len(f_on) else None
def mins(i):
return None if i is None else (df["dtat"].iloc[i]-tc).total_seconds()/60
r = df.iloc[ci]
return dict(cutin_time=tc,
steam_to_cutin=-mins(i_steam),
reflux_to_cutin=(-mins(i_refl) if i_refl is not None else None),
cutin_to_full=mins(i_full),
cutin_rebA=r["reb_temp"], cutin_TC=r["T_C"], cutin_TD=r["T_D"],
cutin_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)
cutins = detect_cutins(df)
print(f"탐지된 ★제품 컷인★(진짜 startup) 이벤트: {len(cutins)}")
rows, windows = [], []
for ci in cutins:
w = df.iloc[max(0, ci-360):min(len(df), ci+360)].copy() # 컷인 ±3h
w["rel_min"] = (w["dtat"] - df["dtat"].iloc[ci]).dt.total_seconds()/60
windows.append(w)
rows.append(milestones(df, ci))
M = pd.DataFrame(rows)
pd.set_option("display.width", 220)
print("\n=== 제품컷인 기준 절차(분) + 컷인 시점 컬럼상태 ===")
cols = ["cutin_time", "steam_to_cutin", "reflux_to_cutin", "cutin_to_full",
"cutin_rebA", "cutin_TC", "cutin_dT_AD"]
show = M[cols].copy()
show["cutin_time"] = show["cutin_time"].dt.strftime("%m-%d %H:%M")
print(show.round(1).to_string(index=False))
print("\n=== 절차 레시피(중앙값) ===")
print(f" 스팀투입→제품컷인(전환류 라인아웃 길이): {M.steam_to_cutin.median():.0f}")
print(f" 리플럭스확립→제품컷인 : {M.reflux_to_cutin.median():.0f}")
print(f" 제품컷인→풀로드 : {M.cutin_to_full.median():.0f}")
print(f" ★제품컷인 트리거(컬럼상태): reb-A={M.cutin_rebA.median():.1f}±{M.cutin_rebA.std():.1f}℃, "
f"T_C={M.cutin_TC.median():.1f}±{M.cutin_TC.std():.2f}℃, ΔT(A-D)={M.cutin_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"ep{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("STARTUP aligned at PRODUCT CUT-IN (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-in")
for a in ax:
a.axvline(0, c="k", lw=.5)
fig.tight_layout(); fig.savefig(BASE + f"{args.prefix}_startup.png", dpi=95)
print(f"\n플롯 저장: {BASE}{args.prefix}_startup.png")
if __name__ == "__main__":
main()