신암정유 6-1차 측류 솔벤트 컬럼(C-6111) 실데이터로 오퍼레이터 모방 제어 분석: - 데이터 추출기 tag_frame() (field_hist WIDE 포맷 디코드) + 운전모드 분류 - ① 생산맵: 스팀유량=f(피드,제품,목표T_C) 운전점 GBM R²0.99 (steam/feed≈0.73) - shadow 백테스트: in-envelope 오퍼레이터 OP 94% 모방, OOD 게이트→폴백 - 롤링 재학습: 새 로드레짐 적응 (5월 OP MAE 3.9→1.2%) - 캠페인내 트림: 컬럼 자기제어, 피드백 미미 → 전향 맵이 제어 지배 - ② START-UP 절차: 레시피 + 제품컷인 트리거(reb-A 84.5℃, ΔT 2℃, 조건기반) 문서: 설계·진행 플랜 + 남은 작업(형제확장·shutdown·assist·live포팅) 작업지시서. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
6.3 KiB
Python
142 lines
6.3 KiB
Python
"""
|
||
C-6111 (6-1차 측류 정제 컬럼) 데이터 추출 + 운전모드 1차 특성 분석.
|
||
|
||
field_hist DB(shinam 실데이터, WIDE 포맷)에서 ptlist/mapping/tblist로 태그를 디코드해
|
||
tidy DataFrame을 만든다. 재사용 가능한 tag_frame() 추출기 포함.
|
||
|
||
근거: docs/학습형제어-오퍼레이터모방-플랜.md §15(디코드), §16(C-6111 토폴로지).
|
||
"""
|
||
import sys
|
||
import psycopg
|
||
import pandas as pd
|
||
|
||
DSN = "host=localhost port=5432 dbname=field_hist user=postgres password=postgres"
|
||
ASSET = "/ASSETS/P6"
|
||
|
||
# C-6111 역할별 태그 (ff_column_config/ff_stream_config + 사용자 도메인, 플랜 §16.1)
|
||
ROLES = {
|
||
"feed": "FICQ-6101.PV", # 피드(주 외란)
|
||
"steam_op": "TICA-6111A.OP", # 리보일러 스팀 밸브(조작/OP)
|
||
"steam_flow": "FIQ-6115.PV", # 실제 스팀 유량
|
||
"reb_temp": "TICA-6111A.PV", # 리보일러 온도(A, 최고온)
|
||
"T_B": "TI-6111B.PV", # 피드존
|
||
"T_C": "TI-6111C.PV", # 민감단(제품 추출 트레이 근처)
|
||
"T_D": "TI-6111D.PV", # 탑상(최저온)
|
||
"feed_preheat": "TI-6103.PV", # 원료 예열
|
||
"vacuum": "PICA-6111.PV", # 진공압력
|
||
"dp": "PI-6111B.PV", # 컬럼 차압
|
||
"product": "FICQ-6118.PV", # 측류 제품 P
|
||
"reflux": "FICQ-6113.PV", # 리플럭스 R
|
||
"light": "FICQ-6114.PV", # 경질분 제거 D
|
||
"heavy": "FICQ-6116.PV", # 중질분 제거 B
|
||
"reb_level": "LI-6111.PV", # 리보일러 레벨
|
||
"reflux_drum": "LICA-6113.PV", # 리플럭스 드럼 레벨
|
||
}
|
||
|
||
|
||
def resolve(conn, shorttags, asset=ASSET):
|
||
"""shortptname 목록 -> {tag: (tblname, colnum)}"""
|
||
with conn.cursor() as cur:
|
||
cur.execute("""
|
||
SELECT p.shortptname, t.tblname, m.oit
|
||
FROM ptlist p JOIN mapping m ON m.pid=p.pid JOIN tblist t ON t.tid=m.tid
|
||
WHERE p.asset=%s AND p.shortptname = ANY(%s)
|
||
""", (asset, list(shorttags)))
|
||
out = {}
|
||
for short, tbl, oit in cur.fetchall():
|
||
out[short] = (tbl, int(oit))
|
||
return out
|
||
|
||
|
||
def tag_frame(conn, role_map, asset=ASSET):
|
||
"""{role: shorttag} -> dtat 인덱스 DataFrame(컬럼=role). 테이블별 1쿼리 후 merge."""
|
||
loc = resolve(conn, role_map.values(), asset)
|
||
missing = [r for r, t in role_map.items() if t not in loc]
|
||
if missing:
|
||
print(f"[warn] 미해결 태그: {[(r, role_map[r]) for r in missing]}", file=sys.stderr)
|
||
# 테이블별 그룹
|
||
by_tbl = {}
|
||
for role, short in role_map.items():
|
||
if short not in loc:
|
||
continue
|
||
tbl, col = loc[short]
|
||
by_tbl.setdefault(tbl, []).append((role, col))
|
||
df = None
|
||
for tbl, cols in by_tbl.items():
|
||
sel = ", ".join([f'col{c:02d} AS "{role}"' for role, c in cols])
|
||
q = f"SELECT dtat, {sel} FROM {tbl}"
|
||
part = pd.read_sql(q, conn)
|
||
df = part if df is None else df.merge(part, on="dtat", how="outer")
|
||
return df.sort_values("dtat").reset_index(drop=True)
|
||
|
||
|
||
def classify_phases(df):
|
||
"""1차 운전모드 분류 (임계 기반, §16.3-2). 추후 정교화."""
|
||
import numpy as np
|
||
reb, vac, steam, prod = df["reb_temp"], df["vacuum"], df["steam_op"], df["product"]
|
||
hot_vac = (reb > 60) & (vac < 200) & (steam > 5) # 컬럼 가동(hot+진공)
|
||
# 온도 추세(60분=120샘플 기울기)로 startup/shutdown 구분
|
||
slope = reb.diff().rolling(120, min_periods=10, center=True).mean()
|
||
mode = np.where(
|
||
hot_vac,
|
||
np.where(prod < 80, "LINEOUT", "PROD"), # 제품≈0 → 전환류/라인아웃
|
||
np.where(slope > 0.02, "STARTUP",
|
||
np.where(slope < -0.02, "SHUTDOWN", "STOPPED")))
|
||
return pd.Series(mode, index=df.index, name="mode")
|
||
|
||
|
||
def plot_timeline(df, png):
|
||
import matplotlib
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt
|
||
d = df.iloc[::30].copy() # 15분 다운샘플
|
||
colors = {"PROD": "#2ca02c", "LINEOUT": "#ff7f0e", "STARTUP": "#1f77b4",
|
||
"SHUTDOWN": "#d62728", "STOPPED": "#7f7f7f"}
|
||
fig, ax = plt.subplots(5, 1, figsize=(16, 12), sharex=True)
|
||
ax[0].plot(d.dtat, d.reb_temp, lw=.5, label="reb_temp(A)")
|
||
ax[0].plot(d.dtat, d.T_C, lw=.5, label="T_C(민감단)")
|
||
ax[0].plot(d.dtat, d.T_D, lw=.5, label="T_D(탑상)")
|
||
ax[0].set_ylabel("온도"); ax[0].legend(loc="upper right", fontsize=7)
|
||
ax[1].plot(d.dtat, d.feed, lw=.5, color="purple"); ax[1].set_ylabel("feed FICQ-6101")
|
||
ax[2].plot(d.dtat, d["product"], lw=.5, color="orange"); ax[2].set_ylabel("측류제품 6118")
|
||
ax[3].plot(d.dtat, d.steam_flow, lw=.5, color="red")
|
||
ax[3].plot(d.dtat, d.steam_op * 10, lw=.5, color="brown", alpha=.5, label="OP×10")
|
||
ax[3].set_ylabel("스팀유량/OP"); ax[3].legend(loc="upper right", fontsize=7)
|
||
ax[4].plot(d.dtat, d.vacuum, lw=.5, color="teal"); ax[4].set_ylabel("진공 PICA-6111")
|
||
ax[4].set_ylim(100, 130)
|
||
# 모드 배경 음영
|
||
for a in ax:
|
||
for m, c in colors.items():
|
||
seg = d[d["mode"] == m]
|
||
a.scatter(seg.dtat, [a.get_ylim()[0]] * len(seg), c=c, s=2, marker="|")
|
||
fig.suptitle("C-6111 (6-1차) 전체기간 — 운전모드별 (하단 컬러바)")
|
||
fig.tight_layout()
|
||
fig.savefig(png, dpi=90)
|
||
print(f"플롯 저장: {png}")
|
||
|
||
|
||
def main():
|
||
with psycopg.connect(DSN) as conn:
|
||
df = tag_frame(conn, ROLES)
|
||
print(f"행수={len(df)} 기간={df.dtat.min()} ~ {df.dtat.max()}")
|
||
print("\n=== 핵심 신호 분포 (운전모드 임계 설정용) ===")
|
||
show = ["feed", "reb_temp", "vacuum", "product", "reflux", "steam_op",
|
||
"steam_flow", "T_C", "T_D", "dp"]
|
||
desc = df[show].describe(percentiles=[.01, .05, .25, .5, .75, .95, .99]).T
|
||
print(desc[["min", "1%", "5%", "50%", "95%", "99%", "max"]].round(2).to_string())
|
||
|
||
df["mode"] = classify_phases(df)
|
||
print("\n=== 운전모드 분포 (30초 샘플 기준) ===")
|
||
vc = df["mode"].value_counts()
|
||
for m, n in vc.items():
|
||
print(f" {m:9s} {n:7d} {100*n/len(df):5.1f}% ≈ {n*30/3600:7.1f} h")
|
||
|
||
out = "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_data.pkl"
|
||
df.to_pickle(out)
|
||
plot_timeline(df, "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_timeline.png")
|
||
print(f"저장: {out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|