feat: 형제 컬럼(6-2~10차) 분석 + SHUTDOWN + operator-assist + C# SteamAdvisor 포팅
- c6111_extract: roles_for() 동적 생성, COLUMN_EXCEPTIONS per-prefix - c6111_prodmap/shadow/startup/rolling: --data/--prefix CLI 인자 지원 - run_column.py: 5개 컬럼 전 파이프라인 실행 래퍼 - c6111_shutdown.py: detect_cutoffs + shutdown_milestones (lookback 1200) - c6111_operator_assist.py: OOD 게이트 + shadow 리플레이 - c6111_export_model.py: 선형근사 JSON export - SteamAdvisor.cs: Predict+ClassifyMode+InEnvelope (NaN guard, Ood fix) - SteamAdvisorController: GET/POST /api/steam/predict - appsettings.json/Program.cs: DI 등록 - docs: 작업지시서 현황 갱신, 진단보고서 작성 (3 MED/8 LOW, 100% 정확도)
This commit is contained in:
80
scripts/analysis/c6111_export_model.py
Normal file
80
scripts/analysis/c6111_export_model.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
모델 JSON export → C# SteamAdvisor에서 로드.
|
||||
|
||||
선형근사(1안): GBM 대신 LinearRegression 계수 export.
|
||||
steam = w0 + w1*feed + w2*product + w3*T_C
|
||||
valve_inv(flow) = poly3 → OP
|
||||
|
||||
사용법:
|
||||
python3 c6111_export_model.py --data c6111_data.pkl --prefix c6111
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.linear_model import LinearRegression
|
||||
|
||||
BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/"
|
||||
FEATURES = ["feed", "product", "T_C"]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
||||
parser.add_argument("--prefix", default="c6111")
|
||||
parser.add_argument("--output", help="JSON 출력 경로 (기본: scripts/analysis/{prefix}_model.json)")
|
||||
args = parser.parse_args()
|
||||
df = pd.read_pickle(args.data)
|
||||
|
||||
prod = df[df["mode"] == "PROD"].copy()
|
||||
prod = prod[(prod["feed"] > 50) & (prod["steam_flow"] > 10) & (prod["steam_op"] > 1)]
|
||||
prod = prod.dropna(subset=FEATURES + ["steam_op", "steam_flow"])
|
||||
ops = (prod.set_index("dtat").resample("6h").median(numeric_only=True)
|
||||
.dropna(subset=["steam_flow", "feed"]))
|
||||
ops = ops[ops["feed"] > 50]
|
||||
|
||||
# 선형 모델
|
||||
lr = LinearRegression()
|
||||
lr.fit(ops[FEATURES].values, ops["steam_flow"].values)
|
||||
r2 = lr.score(ops[FEATURES].values, ops["steam_flow"].values)
|
||||
print(f"선형 steam_flow R² = {r2:.4f} (GBM 대비 비교용)")
|
||||
|
||||
# 밸브 역특성: steam_flow → steam_op (3차)
|
||||
vp = np.polyfit(prod["steam_flow"], prod["steam_op"], 3)
|
||||
|
||||
# Envelope (1%, 99%)
|
||||
lo = ops[FEATURES].quantile(0.01)
|
||||
hi = ops[FEATURES].quantile(0.99)
|
||||
|
||||
# GBM feature importance (참고용)
|
||||
try:
|
||||
from sklearn.ensemble import GradientBoostingRegressor
|
||||
gbm = GradientBoostingRegressor(n_estimators=200, max_depth=2,
|
||||
learning_rate=0.05, random_state=0)
|
||||
gbm.fit(ops[FEATURES].values, ops["steam_flow"].values)
|
||||
gbm_r2 = gbm.score(ops[FEATURES].values, ops["steam_flow"].values)
|
||||
except Exception:
|
||||
gbm_r2 = None
|
||||
|
||||
model = {
|
||||
"column": args.prefix,
|
||||
"features": FEATURES,
|
||||
"linear_coeffs": lr.coef_.tolist(),
|
||||
"intercept": lr.intercept_,
|
||||
"linear_r2": round(r2, 4),
|
||||
"gbm_r2": round(gbm_r2, 4) if gbm_r2 else None,
|
||||
"valve_poly": vp.tolist(),
|
||||
"envelope_lo": {c: round(float(lo[c]), 1) for c in FEATURES},
|
||||
"envelope_hi": {c: round(float(hi[c]), 1) for c in FEATURES},
|
||||
"n_operating_points": len(ops),
|
||||
"n_prod_rows": len(prod),
|
||||
}
|
||||
out = args.output or (BASE + f"{args.prefix}_model.json")
|
||||
with open(out, "w") as f:
|
||||
json.dump(model, f, indent=2)
|
||||
print(f"\n모델 export: {out}")
|
||||
print(json.dumps(model, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,10 +1,17 @@
|
||||
"""
|
||||
C-6111 (6-1차 측류 정제 컬럼) 데이터 추출 + 운전모드 1차 특성 분석.
|
||||
컬럼 데이터 추출 + 운전모드 1차 특성 분석.
|
||||
|
||||
field_hist DB(shinam 실데이터, WIDE 포맷)에서 ptlist/mapping/tblist로 태그를 디코드해
|
||||
tidy DataFrame을 만든다. 재사용 가능한 tag_frame() 추출기 포함.
|
||||
|
||||
근거: docs/학습형제어-오퍼레이터모방-플랜.md §15(디코드), §16(C-6111 토폴로지).
|
||||
|
||||
형제 컬럼 확장: roles_for(prefix, asset)로 파라미터화.
|
||||
- 6-1: prefix=61, asset=/ASSETS/P6 (기본)
|
||||
- 6-2: prefix=62, asset=/ASSETS/P6
|
||||
- 8: prefix=81, asset=/ASSETS/P8
|
||||
- 9: prefix=91, asset=/ASSETS/P9 (또는 92)
|
||||
- 10: prefix=101, asset=/ASSETS/P10 (또는 102)
|
||||
"""
|
||||
import sys
|
||||
import psycopg
|
||||
@@ -13,27 +20,78 @@ 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", # 리플럭스 드럼 레벨
|
||||
# --- 형제 컬럼 역할 생성기 ---
|
||||
# DB 검증 결과(2026-06-05) 기반 예외 오버라이드:
|
||||
# P8(81): TICA에 A/B/C/D 접미사 없음, PICA-8111A (with A suffix)
|
||||
# P9(91): PICA-9111A (with A suffix). 92xx 2차 컬럼 존재
|
||||
# P10(101): FICQ-10114A (not 10114), PICA-10111A, LIA-10111 (not LICA). 102xx 2차 컬럼 존재
|
||||
COLUMN_EXCEPTIONS = {
|
||||
"81": {
|
||||
"steam_op": "TICA-8111.OP",
|
||||
"reb_temp": "TICA-8111.PV",
|
||||
"vacuum": "PICA-8111A.PV",
|
||||
},
|
||||
"91": {
|
||||
"vacuum": "PICA-9111A.PV",
|
||||
},
|
||||
"92": {
|
||||
"vacuum": "PICA-9211A.PV",
|
||||
},
|
||||
"101": {
|
||||
"light": "FICQ-10114A.PV",
|
||||
"vacuum": "PICA-10111A.PV",
|
||||
"reflux_drum": "LIA-10111.PV",
|
||||
},
|
||||
"102": {
|
||||
"light": "FICQ-10214.PV",
|
||||
"vacuum": "PICA-10211A.PV",
|
||||
"reflux_drum": "LIA-10211.PV",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def roles_for(prefix, asset=ASSET):
|
||||
"""{role: shorttag} dict 생성. prefix 예: '61', '62', '81', '91', '101'.
|
||||
|
||||
Base 규칙(6-1 기준, docs/작업지시서-학습형제어-다음단계.md 작업1):
|
||||
feed=FICQ-{p}01, reflux=FICQ-{p}13, light(D)=FICQ-{p}14,
|
||||
heavy(B)=FICQ-{p}16, product(P)=FICQ-{p}18,
|
||||
steam_op=TICA-{p}11A.OP, reb_temp=TICA-{p}11A.PV,
|
||||
steam_flow=FIQ-{p}15, T_B=TI-{p}11B, T_C=TI-{p}11C, T_D=TI-{p}11D,
|
||||
vacuum=PICA-{p}11.PV, dp=PI-{p}11B.PV,
|
||||
reb_level=LI-{p}11.PV, reflux_drum=LICA-{p}13.PV,
|
||||
feed_preheat=TI-{p}03.PV
|
||||
|
||||
COLUMN_EXCEPTIONS에 등록된 prefix는 자동 오버라이드.
|
||||
"""
|
||||
p = prefix
|
||||
roles = {
|
||||
"feed": f"FICQ-{p}01.PV",
|
||||
"steam_op": f"TICA-{p}11A.OP",
|
||||
"steam_flow": f"FIQ-{p}15.PV",
|
||||
"reb_temp": f"TICA-{p}11A.PV",
|
||||
"T_B": f"TI-{p}11B.PV",
|
||||
"T_C": f"TI-{p}11C.PV",
|
||||
"T_D": f"TI-{p}11D.PV",
|
||||
"feed_preheat": f"TI-{p}03.PV",
|
||||
"vacuum": f"PICA-{p}11.PV",
|
||||
"dp": f"PI-{p}11B.PV",
|
||||
"product": f"FICQ-{p}18.PV",
|
||||
"reflux": f"FICQ-{p}13.PV",
|
||||
"light": f"FICQ-{p}14.PV",
|
||||
"heavy": f"FICQ-{p}16.PV",
|
||||
"reb_level": f"LI-{p}11.PV",
|
||||
"reflux_drum": f"LICA-{p}13.PV",
|
||||
}
|
||||
ov = COLUMN_EXCEPTIONS.get(prefix, {})
|
||||
roles.update(ov)
|
||||
return roles
|
||||
|
||||
|
||||
# C-6111 (6-1) 역할별 태그 — legacy 직접 참조 호환용
|
||||
ROLES = roles_for("61", ASSET)
|
||||
|
||||
|
||||
def resolve(conn, shorttags, asset=ASSET):
|
||||
"""shortptname 목록 -> {tag: (tblname, colnum)}"""
|
||||
with conn.cursor() as cur:
|
||||
|
||||
193
scripts/analysis/c6111_operator_assist.py
Normal file
193
scripts/analysis/c6111_operator_assist.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Operator-assist 패키징 (작업3).
|
||||
|
||||
사용법:
|
||||
python3 c6111_operator_assist.py --data c61_data.pkl --prefix c61
|
||||
python3 c6111_operator_assist.py --data c61_data.pkl --prefix c61 --live '{"feed":500,"product":300,"T_C":84.7}'
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import IsolationForest
|
||||
|
||||
BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/"
|
||||
FEATURES = ["feed", "product", "T_C"]
|
||||
PROD_SMOOTH = 40
|
||||
|
||||
|
||||
class OperatorAssist:
|
||||
def __init__(self, df):
|
||||
self.df = df
|
||||
self.mode = "UNKNOWN"
|
||||
self.model = None
|
||||
self.inv = None
|
||||
self.ood = None
|
||||
self.env_lo = None
|
||||
self.env_hi = None
|
||||
self._train()
|
||||
|
||||
def _train(self):
|
||||
prod = self.df[self.df["mode"] == "PROD"].copy()
|
||||
prod = prod[(prod["feed"] > 50) & (prod["steam_flow"] > 10) & (prod["steam_op"] > 1)]
|
||||
prod = prod.dropna(subset=FEATURES + ["steam_op", "steam_flow"])
|
||||
if len(prod) < 100:
|
||||
print(" [WARN] PROD 데이터 부족 — advisory 신뢰도 낮음")
|
||||
points = (prod.set_index("dtat").resample("6h").median(numeric_only=True)
|
||||
.dropna(subset=["steam_flow", "feed"]))
|
||||
points = points[points["feed"] > 50]
|
||||
from sklearn.ensemble import GradientBoostingRegressor
|
||||
self.model = GradientBoostingRegressor(n_estimators=200, max_depth=2,
|
||||
learning_rate=0.05, random_state=0)
|
||||
self.model.fit(points[FEATURES].values, points["steam_flow"].values)
|
||||
self.inv = np.polyfit(prod["steam_flow"], prod["steam_op"], 3)
|
||||
self.env_lo = points[FEATURES].quantile(0.01)
|
||||
self.env_hi = points[FEATURES].quantile(0.99)
|
||||
self.ood = IsolationForest(contamination=0.05, random_state=0).fit(points[FEATURES].values)
|
||||
print(f" 학습 운전점: {len(points)}개 envelope:")
|
||||
for c in FEATURES:
|
||||
print(f" {c}: [{self.env_lo[c]:.0f}, {self.env_hi[c]:.1f}]")
|
||||
|
||||
def classify_mode(self, tags):
|
||||
"""tags dict → mode 추정 (classify_phases 단순 replica).
|
||||
|
||||
steam_op 없으면 feed/product로 판단 (live advisory용).
|
||||
"""
|
||||
prod = tags.get("product", 0)
|
||||
feed = tags.get("feed", 0)
|
||||
steam = tags.get("steam_op", None)
|
||||
reb = tags.get("reb_temp", 60)
|
||||
if prod > 100:
|
||||
if steam is None or steam > 10:
|
||||
return "PROD"
|
||||
if steam is not None:
|
||||
if steam > 10 and reb > 60:
|
||||
return "LINEOUT"
|
||||
if steam > 10 and feed < 50:
|
||||
return "STARTUP"
|
||||
if feed > 50:
|
||||
return "PROD" # fallback: steam_op 없이 feed>50 + product>100는 PROD
|
||||
return "STOPPED"
|
||||
|
||||
def in_envelope(self, tags):
|
||||
x = np.array([[tags[c] for c in FEATURES]])
|
||||
return ((x >= self.env_lo.values) & (x <= self.env_hi.values)).all()
|
||||
|
||||
def ood_score(self, tags):
|
||||
return self.ood.decision_function(np.array([[tags[c] for c in FEATURES]]))[0]
|
||||
|
||||
def predict(self, tags, smooth_history=None):
|
||||
"""live_tags dict → advisory dict.
|
||||
|
||||
tags: {"feed": float, "product": float, "T_C": float}
|
||||
smooth_history: optional list of prior tag dicts for causal smoothing
|
||||
|
||||
Returns:
|
||||
{"rec_OP": float or None, "rec_steam": float, "confidence": str,
|
||||
"mode": str, "ood": bool, "in_env": bool, "message": str}
|
||||
"""
|
||||
mode = self.classify_mode(tags)
|
||||
self.mode = mode
|
||||
env = self.in_envelope(tags)
|
||||
ood = self.ood_score(tags) < 0
|
||||
raw = np.array([[[tags[c] for c in FEATURES]]])
|
||||
|
||||
if mode != "PROD":
|
||||
msg = f"운전모드={mode} — advisory는 PROD에서만 제공 (STARTUP/LINEOUT은 레시피 참조)"
|
||||
return {"rec_OP": None, "rec_steam": None, "confidence": "N/A",
|
||||
"mode": mode, "ood": ood, "in_env": env, "message": msg}
|
||||
|
||||
# smooth: causal trailing median over recent history
|
||||
if smooth_history and len(smooth_history) >= PROD_SMOOTH:
|
||||
buf = pd.DataFrame(smooth_history[-PROD_SMOOTH:])[FEATURES].median()
|
||||
x = np.array([[buf[c] for c in FEATURES]])
|
||||
else:
|
||||
x = raw[0]
|
||||
|
||||
sf = self.model.predict(x)[0]
|
||||
op = np.clip(np.polyval(self.inv, sf), 0, 100)
|
||||
|
||||
if not env:
|
||||
confidence = "LOW_OOD"
|
||||
msg = (f"⚠ 범위밖 입력 — 권장 OP={op:.1f}% (외삽, 신뢰도 낮음). "
|
||||
"오퍼레이터 판단 우선")
|
||||
elif ood:
|
||||
confidence = "MEDIUM"
|
||||
msg = f"권장 OP={op:.1f}% (신뢰: 구간내, IForest 이상감지 — 주의)"
|
||||
else:
|
||||
confidence = "HIGH"
|
||||
msg = f"권장 OP={op:.1f}% (신뢰: 구간내)"
|
||||
|
||||
return {"rec_OP": round(op, 1), "rec_steam": round(sf, 1),
|
||||
"confidence": confidence, "mode": mode, "ood": bool(ood),
|
||||
"in_env": bool(env), "feed": float(x[0][0]),
|
||||
"product": float(x[0][1]), "T_C": float(x[0][2]),
|
||||
"message": msg}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
||||
parser.add_argument("--prefix", default="c6111")
|
||||
parser.add_argument("--live", help='JSON live_tags for single predict test')
|
||||
args = parser.parse_args()
|
||||
df = pd.read_pickle(args.data)
|
||||
assist = OperatorAssist(df)
|
||||
|
||||
if args.live:
|
||||
tags = json.loads(args.live)
|
||||
res = assist.predict(tags)
|
||||
print(f"\n=== Operator Advisory ({args.prefix}) ===")
|
||||
for k, v in res.items():
|
||||
print(f" {k:15s}: {v}")
|
||||
return
|
||||
|
||||
# 전체 shadow 리플레이: PROD 행 벡터화 처리
|
||||
prod = df[df["mode"] == "PROD"].sort_values("dtat").copy()
|
||||
prod = prod[(prod["feed"] > 50) & (prod["steam_flow"] > 10) & (prod["steam_op"] > 1)
|
||||
& prod[FEATURES + ["steam_op"]].notna().all(axis=1)]
|
||||
if len(prod) == 0:
|
||||
print(" PROD 없음 — advisory 불가")
|
||||
return
|
||||
|
||||
X = prod[FEATURES].values
|
||||
sf = assist.model.predict(X)
|
||||
op = np.clip(np.polyval(assist.inv, sf), 0, 100)
|
||||
env_mask = ((X >= assist.env_lo.values) & (X <= assist.env_hi.values)).all(axis=1)
|
||||
ood_mask = assist.ood.decision_function(X) < 0
|
||||
errors = op - prod["steam_op"].values
|
||||
|
||||
ood_rate = np.mean(ood_mask) * 100
|
||||
within_2 = np.mean(np.abs(errors) <= 2.0) * 100
|
||||
print(f"\n=== Shadow Advisory Report ({args.prefix}) ===")
|
||||
print(f" PROD 행수 : {len(prod)}")
|
||||
print(f" OOD 비율 : {ood_rate:.1f}%")
|
||||
print(f" OP MAE : {np.abs(errors).mean():.2f}%")
|
||||
print(f" |Δ|≤2% : {within_2:.1f}% (검증기준: 90%+ in-envelope)")
|
||||
env_only = errors[~ood_mask[:len(errors)]]
|
||||
if len(env_only):
|
||||
print(f" in-env MAE : {np.abs(env_only).mean():.2f}% "
|
||||
f"|Δ|≤2%={np.mean(np.abs(env_only)<=2)*100:.1f}%")
|
||||
|
||||
# 권장 OP vs 실제 OP 시계열 플롯
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
fig, ax = plt.subplots(2, 1, figsize=(14, 8))
|
||||
s = prod.iloc[::10]
|
||||
ax[0].plot(s["dtat"], s["steam_op"], lw=.6, label="actual OP")
|
||||
ax[0].plot(s["dtat"], op[::10], lw=.6, c="r", label="advisory OP")
|
||||
ax[0].set_ylabel("OP %"); ax[0].legend(fontsize=8)
|
||||
ax[0].set_title(f"Operator Advisory vs Actual OP ({args.prefix})")
|
||||
ax[1].hist(errors, bins=60)
|
||||
ax[1].axvline(0, c="k", lw=.5)
|
||||
ax[1].set_title(f"Advisory error (rec-actual): median {np.median(errors):+.2f}%, "
|
||||
f"within 2%={within_2:.1f}%")
|
||||
fig.tight_layout()
|
||||
path = BASE + f"{args.prefix}_advisory.png"
|
||||
fig.savefig(path, dpi=95)
|
||||
print(f"\n 플롯 저장: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,14 +1,12 @@
|
||||
"""
|
||||
C-6111 ① 생산 정상상태 맵 (플랜 §16.3-5).
|
||||
① 생산 정상상태 맵.
|
||||
|
||||
PROD 구간에서:
|
||||
1) 밸브특성: OP(TICA-6111A.OP) ↔ 스팀유량(FIQ-6115) — stiction/비선형/게인
|
||||
2) 정상상태 세그먼트 추출
|
||||
3) 회귀: 스팀유량 = f(피드, 리플럭스, 제품, 진공, ΔT…) + 피처중요도 + 시간분할 검증
|
||||
→ "오퍼레이터 스팀이 가용변수로 얼마나 설명되나" (FIT/MAE)
|
||||
PROD 구간에서 밸브특성 + 스팀유량 회귀.
|
||||
|
||||
선행: c6111_extract.py 가 만든 c6111_data.pkl (mode 컬럼 포함).
|
||||
형제 컬럼 호환: --data, --prefix CLI 인자.
|
||||
"""
|
||||
import argparse
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
@@ -20,14 +18,15 @@ from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.metrics import r2_score, mean_absolute_error
|
||||
|
||||
BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/"
|
||||
TARGET = "steam_flow" # FIQ-6115 (에너지). 비교용으로 steam_op도 출력
|
||||
# 깨끗한 인과입력만 (reflux/dp/dT는 스팀의 결과·동시조작 → 순환참조라 제외)
|
||||
TARGET = "steam_flow"
|
||||
FEATURES = ["feed", "product", "vacuum", "feed_preheat", "T_C", "T_D"]
|
||||
OP_RESAMPLE = "6h" # 운전점 집계 (정상상태 내부 변동 적음 → 캠페인/로드레벨 단위 학습)
|
||||
OP_RESAMPLE = "6h"
|
||||
|
||||
|
||||
def load():
|
||||
df = pd.read_pickle(BASE + "c6111_data.pkl")
|
||||
def load(data_path=None):
|
||||
if data_path is None:
|
||||
data_path = BASE + "c6111_data.pkl"
|
||||
df = pd.read_pickle(data_path)
|
||||
df = df[df["mode"] == "PROD"].copy()
|
||||
# 엔지니어링 피처: 온도 구배(분리도)
|
||||
df["dT_AC"] = df["reb_temp"] - df["T_C"]
|
||||
@@ -101,31 +100,35 @@ def regress(df):
|
||||
return ops, gbm, Xte, yte, gbm.predict(Xte), imp
|
||||
|
||||
|
||||
def plots(hb, ops, yte, pred, imp):
|
||||
def plots(hb, ops, yte, pred, imp, prefix="c6111"):
|
||||
fig, ax = plt.subplots(1, 4, figsize=(22, 5))
|
||||
ax[0].scatter(hb["op"], hb["flow"], s=20, c="k", label="mean")
|
||||
ax[0].plot(hb["op"], hb["flow_up"], "b.-", ms=4, label="OP rising")
|
||||
ax[0].plot(hb["op"], hb["flow_dn"], "r.-", ms=4, label="OP falling")
|
||||
ax[0].set_xlabel("steam OP %"); ax[0].set_ylabel("steam flow FIQ-6115")
|
||||
ax[0].set_xlabel("steam OP %"); ax[0].set_ylabel("steam flow")
|
||||
ax[0].set_title("Valve char (hysteresis=stiction)"); ax[0].legend()
|
||||
ax[1].scatter(ops["feed"], ops[TARGET], s=10, alpha=.5)
|
||||
ax[1].set_xlabel("feed FICQ-6101"); ax[1].set_ylabel("steam flow")
|
||||
ax[1].set_xlabel("feed"); ax[1].set_ylabel("steam flow")
|
||||
ax[1].set_title("steam vs feed (operating points)")
|
||||
ax[2].scatter(yte, pred, s=12, alpha=.5)
|
||||
lim = [min(yte.min(), pred.min()), max(yte.max(), pred.max())]
|
||||
ax[2].plot(lim, lim, "r--"); ax[2].set_xlabel("actual steam flow")
|
||||
ax[2].set_ylabel("predicted (GBM)"); ax[2].set_title("Predicted vs Actual (test ops)")
|
||||
imp.sort_values().plot.barh(ax=ax[3]); ax[3].set_title("GBM feature importance")
|
||||
fig.tight_layout(); fig.savefig(BASE + "c6111_prodmap.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}c6111_prodmap.png")
|
||||
fig.tight_layout(); fig.savefig(BASE + f"{prefix}_prodmap.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}{prefix}_prodmap.png")
|
||||
|
||||
|
||||
def main():
|
||||
df = load()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
||||
parser.add_argument("--prefix", default="c6111")
|
||||
args = parser.parse_args()
|
||||
df = load(args.data)
|
||||
print(f"PROD 정합데이터 {len(df)}행")
|
||||
hb, a = valve_char(df)
|
||||
ops, gbm, Xte, yte, pred, imp = regress(df)
|
||||
plots(hb, ops, yte, pred, imp)
|
||||
plots(hb, ops, yte, pred, imp, args.prefix)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""
|
||||
C-6111 롤링(walk-forward) 재학습 — OOD/외삽 바이어스 해소 데모 (플랜 §16.7-(1)).
|
||||
롤링(walk-forward) 재학습 — OOD/외삽 바이어스 해소 데모.
|
||||
|
||||
held-out 5월을 하루씩 전진하며 '그 날 이전 전체 이력(expanding window)'으로 매일 재학습→그 날 예측.
|
||||
정적 모델(2~4월 고정)의 +4% 외삽 바이어스가 모델이 5월 저부하 데이터를 흡수하며
|
||||
사라지는지(적응 곡선) + OOD 비율이 떨어지는지 확인. 입력 평활은 인과(trailing).
|
||||
형제 컬럼 호환: --data, --prefix CLI 인자.
|
||||
"""
|
||||
import argparse
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
@@ -18,7 +17,11 @@ RETRAIN_EVERY = "1D"
|
||||
|
||||
|
||||
def main():
|
||||
df = pd.read_pickle(BASE + "c6111_data.pkl")
|
||||
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)
|
||||
df = df[df["mode"] == "PROD"].copy()
|
||||
df = df[(df["feed"] > 50) & (df["steam_flow"] > 10) & (df["steam_op"] > 1)
|
||||
& df[FEATURES + ["steam_op"]].notna().all(axis=1)].sort_values("dtat")
|
||||
@@ -27,6 +30,10 @@ def main():
|
||||
df[c + "_s"] = df[c].rolling(SMOOTH, min_periods=1).median()
|
||||
|
||||
ho = pd.Timestamp(HELDOUT_START)
|
||||
if df["dtat"].max() < ho:
|
||||
print(f"데이터 종료 {df.dtat.max()} < HELDOUT_START({ho}) — 롤링 재학습 불가. (컬럼 가동기간이 5월 이전)")
|
||||
return
|
||||
|
||||
days = pd.date_range(ho, df["dtat"].max(), freq=RETRAIN_EVERY)
|
||||
|
||||
# 정적 모델: 5월 이전 전체로 1회 학습
|
||||
@@ -70,8 +77,8 @@ def main():
|
||||
ax[0].set_ylabel("OP MAE %"); ax[0].legend(); ax[0].set_title("Rolling vs static — adaptation over May")
|
||||
ax[1].plot(r.day, r.ood_roll, "b.-"); ax[1].set_ylabel("rolling OOD %")
|
||||
ax[1].set_title("OOD fraction (학습 envelope 밖) — 5월 데이터 흡수하며 감소")
|
||||
fig.tight_layout(); fig.savefig(BASE + "c6111_rolling.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}c6111_rolling.png")
|
||||
fig.tight_layout(); fig.savefig(BASE + f"{args.prefix}_rolling.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}{args.prefix}_rolling.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"""
|
||||
C-6111 Shadow 예측기 — 히스토리 리플레이 백테스트 (플랜 §7 shadow 진입).
|
||||
Shadow 예측기 — 히스토리 리플레이 백테스트.
|
||||
|
||||
학습기간 운전점으로 `스팀유량=f(피드,제품,목표T_C)` 학습 → held-out 미래기간을
|
||||
매 시점 리플레이하여 예측 스팀→(밸브 역특성)→예측 OP 를 산출, **실제 오퍼레이터 OP와 비교**.
|
||||
"이 예측기를 shadow로 돌렸다면 오퍼레이터 손과 얼마나 일치했나" 를 정직 검증.
|
||||
|
||||
선행: c6111_data.pkl. 포팅대상(추후 C# live shadow)은 동일 로직.
|
||||
선행: c6111_data.pkl. 형제 컬럼 호환: --data, --prefix CLI 인자.
|
||||
"""
|
||||
import argparse
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
@@ -16,9 +13,9 @@ from sklearn.ensemble import GradientBoostingRegressor
|
||||
from sklearn.metrics import r2_score, mean_absolute_error
|
||||
|
||||
BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/"
|
||||
FEATURES = ["feed", "product", "T_C"] # 깨끗한 인과/목표 입력 (§16.6)
|
||||
SMOOTH = 40 # 입력 평활 20분(운전점 성격 유지)
|
||||
TRAIN_FRAC = 0.70 # 앞 70% 기간 학습, 뒤 30% held-out shadow
|
||||
FEATURES = ["feed", "product", "T_C"]
|
||||
SMOOTH = 40
|
||||
TRAIN_FRAC = 0.70
|
||||
|
||||
|
||||
class SteamPredictor:
|
||||
@@ -42,7 +39,11 @@ class SteamPredictor:
|
||||
|
||||
|
||||
def main():
|
||||
df = pd.read_pickle(BASE + "c6111_data.pkl")
|
||||
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)
|
||||
df = df[df["mode"] == "PROD"].copy()
|
||||
df = df[(df["feed"] > 50) & (df["steam_flow"] > 10) & (df["steam_op"] > 1)
|
||||
& df[FEATURES + ["steam_op"]].notna().all(axis=1)].sort_values("dtat")
|
||||
@@ -91,8 +92,8 @@ def main():
|
||||
err = te["pred_op"] - te["steam_op"]
|
||||
ax[2].hist(err, bins=80); ax[2].axvline(0, c="k", lw=.5)
|
||||
ax[2].set_title(f"OP error (pred-actual): median {err.median():+.2f}%, std {err.std():.2f}%")
|
||||
fig.tight_layout(); fig.savefig(BASE + "c6111_shadow.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}c6111_shadow.png")
|
||||
fig.tight_layout(); fig.savefig(BASE + f"{args.prefix}_shadow.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}{args.prefix}_shadow.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
148
scripts/analysis/c6111_shutdown.py
Normal file
148
scripts/analysis/c6111_shutdown.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
③ 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()
|
||||
@@ -1,11 +1,9 @@
|
||||
"""
|
||||
C-6111 ② START-UP 절차 학습 (플랜 §16.4 ②, few-shot).
|
||||
② START-UP 절차 학습 (few-shot).
|
||||
|
||||
startup 에피소드를 탐지→스팀투입 시점(t0)에 정렬→중첩, 절차를 해석가능 레시피로 추출:
|
||||
단계 시퀀스(진공→스팀/승온→전환류 라인아웃→제품컷인→로드램프→생산),
|
||||
각 단계 타이밍, 그리고 ★핵심 결정 "제품 컷인" 시점의 컬럼 상태(트리거)★.
|
||||
블랙박스 정책 아님 — 안전·설명가능 우선.
|
||||
형제 컬럼 호환: --data, --prefix CLI 인자.
|
||||
"""
|
||||
import argparse
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
@@ -61,7 +59,11 @@ def milestones(df, ci):
|
||||
|
||||
|
||||
def main():
|
||||
df = pd.read_pickle(BASE + "c6111_data.pkl").sort_values("dtat").reset_index(drop=True)
|
||||
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)
|
||||
cutins = detect_cutins(df)
|
||||
print(f"탐지된 ★제품 컷인★(진짜 startup) 이벤트: {len(cutins)}개")
|
||||
|
||||
@@ -101,8 +103,8 @@ def main():
|
||||
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 + "c6111_startup.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}c6111_startup.png")
|
||||
fig.tight_layout(); fig.savefig(BASE + f"{args.prefix}_startup.png", dpi=95)
|
||||
print(f"\n플롯 저장: {BASE}{args.prefix}_startup.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
173
scripts/analysis/run_column.py
Normal file
173
scripts/analysis/run_column.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
형제 컬럼 확장(작업1) 일괄 실행 래퍼.
|
||||
|
||||
사용법:
|
||||
python3 run_column.py --prefix 62 # 6-2차 단독
|
||||
python3 run_column.py --prefix 81 --asset /ASSETS/P8
|
||||
python3 run_column.py --all # 모든 형제 컬럼
|
||||
"""
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import psycopg
|
||||
import pandas as pd
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
COLUMNS = [
|
||||
("61", "/ASSETS/P6", "C-6111 (6-1차)"),
|
||||
("62", "/ASSETS/P6", "C-6211 (6-2차)"),
|
||||
("81", "/ASSETS/P8", "C-8111 (8차)"),
|
||||
("91", "/ASSETS/P9", "C-9111 (9차)"),
|
||||
("101", "/ASSETS/P10", "C-10111 (10차)"),
|
||||
]
|
||||
|
||||
PREFIX_ASSET = {p: a for p, a, _ in COLUMNS}
|
||||
|
||||
DSN = "host=localhost port=5432 dbname=field_hist user=postgres password=postgres"
|
||||
PY = sys.executable
|
||||
|
||||
|
||||
def extract(prefix, asset):
|
||||
"""추출 + 운전모드 분류. c{prefix}_data.pkl 저장."""
|
||||
from c6111_extract import roles_for, tag_frame, classify_phases
|
||||
|
||||
with psycopg.connect(DSN) as conn:
|
||||
roles = roles_for(prefix, asset)
|
||||
print(f"\n ROLES ({len(roles)}):")
|
||||
for k, v in roles.items():
|
||||
print(f" {k:15s} -> {v}")
|
||||
df = tag_frame(conn, roles, asset)
|
||||
|
||||
df["mode"] = classify_phases(df)
|
||||
out = os.path.join(BASE, f"c{prefix}_data.pkl")
|
||||
df.to_pickle(out)
|
||||
|
||||
print(f"\n=== {prefix} ({asset}) ===")
|
||||
print(f" 행수={len(df)} 기간={df.dtat.min()} ~ {df.dtat.max()}")
|
||||
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:.1f}h")
|
||||
print(f" 저장: {out}")
|
||||
return out
|
||||
|
||||
|
||||
def run_analysis(script, prefix):
|
||||
"""분석 스크립트 1개 실행 (subprocess)."""
|
||||
data = os.path.join(BASE, f"c{prefix}_data.pkl")
|
||||
cmd = [PY, os.path.join(BASE, script), "--data", data, "--prefix", f"c{prefix}"]
|
||||
print(f"\n>>> {' '.join(cmd)}")
|
||||
r = subprocess.run(cmd)
|
||||
return r.returncode
|
||||
|
||||
|
||||
def run_column(prefix, asset, label):
|
||||
"""컬럼 1개 전체 파이프라인."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {label} (prefix={prefix}, asset={asset})")
|
||||
print(f"{'='*60}")
|
||||
extract(prefix, asset)
|
||||
for script in ["c6111_prodmap.py", "c6111_shadow.py", "c6111_rolling.py", "c6111_startup.py", "c6111_shutdown.py", "c6111_operator_assist.py", "c6111_export_model.py"]:
|
||||
rc = run_analysis(script, prefix)
|
||||
if rc != 0:
|
||||
print(f" [WARN] {script} → exit {rc}")
|
||||
|
||||
|
||||
def compare():
|
||||
"""모든 컬럼 결과 취합 → 비교표 (prodmap + shadow + startup)."""
|
||||
import numpy as np
|
||||
|
||||
rows = []
|
||||
for prefix, asset, label in COLUMNS:
|
||||
pkl = os.path.join(BASE, f"c{prefix}_data.pkl")
|
||||
# 6-1 legacy: c6111_data.pkl (not c61_data.pkl)
|
||||
if prefix == "61" and not os.path.exists(pkl):
|
||||
alt = os.path.join(BASE, "c6111_data.pkl")
|
||||
if os.path.exists(alt):
|
||||
pkl = alt
|
||||
if not os.path.exists(pkl):
|
||||
print(f" [skip] {label}: {pkl} 없음")
|
||||
continue
|
||||
df = pd.read_pickle(pkl)
|
||||
prod = df[df["mode"] == "PROD"]
|
||||
steam_feed = prod["steam_flow"].median() / prod["feed"].median() if len(prod) else float("nan")
|
||||
total_h = len(df) * 30 / 3600
|
||||
prod_h = len(prod) * 30 / 3600
|
||||
|
||||
# 컷인 탐지 (startup.py detect_cutins 로직 인라인)
|
||||
prod_arr = df["product"].values
|
||||
reb_arr = df["reb_temp"].values
|
||||
dtat_vals = df["dtat"].values
|
||||
cutins = []
|
||||
i = 60
|
||||
n = len(df)
|
||||
while i < n:
|
||||
if prod_arr[i] > 100 and prod_arr[i-1] <= 100:
|
||||
pre = prod_arr[max(0, i-60):i]
|
||||
if np.nanmedian(pre) < 50 and reb_arr[i] > 75:
|
||||
cutins.append(i)
|
||||
i += 720
|
||||
continue
|
||||
i += 1
|
||||
|
||||
row = {"컬럼": label,
|
||||
"기간": f"{df['dtat'].min():%m-%d}~{df['dtat'].max():%m-%d}",
|
||||
"전체(h)": f"{total_h:.0f}",
|
||||
"PROD%": f"{100*len(prod)/len(df):.1f}",
|
||||
"생산(h)": f"{prod_h:.0f}",
|
||||
"steam/feed": f"{steam_feed:.3f}",
|
||||
"컷인": str(len(cutins))}
|
||||
|
||||
if cutins:
|
||||
cutin_data = []
|
||||
for ci in cutins:
|
||||
cutin_data.append({"reb": df.loc[ci, "reb_temp"],
|
||||
"tc": df.loc[ci, "T_C"],
|
||||
"dT": df.loc[ci, "reb_temp"] - df.loc[ci, "T_D"]})
|
||||
cdf = pd.DataFrame(cutin_data)
|
||||
row["컷인_reb-A"] = f"{cdf['reb'].median():.1f}±{cdf['reb'].std():.1f}"
|
||||
row["컷인_dT_AD"] = f"{cdf['dT'].median():.1f}±{cdf['dT'].std():.1f}"
|
||||
else:
|
||||
row["컷인_reb-A"] = ""
|
||||
row["컷인_dT_AD"] = ""
|
||||
|
||||
rows.append(row)
|
||||
|
||||
pd.set_option("display.width", 300)
|
||||
pd.set_option("display.max_columns", 20)
|
||||
print("\n\n" + "="*120)
|
||||
print(" 형제 컬럼 비교표")
|
||||
print("="*120)
|
||||
tbl = pd.DataFrame(rows).set_index("컬럼")
|
||||
print(tbl.to_string())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="형제 컬럼 확장 일괄 실행")
|
||||
parser.add_argument("--prefix", help="컬럼 prefix (61, 62, 81, 91, 101)")
|
||||
parser.add_argument("--asset", help="asset 경로 (예: /ASSETS/P6)")
|
||||
parser.add_argument("--all", action="store_true", help="모든 형제 컬럼 실행")
|
||||
parser.add_argument("--compare", action="store_true", help="기존 pkl로 비교표만 출력")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.compare:
|
||||
compare()
|
||||
elif args.all:
|
||||
for prefix, asset, label in COLUMNS:
|
||||
run_column(prefix, asset, label)
|
||||
compare()
|
||||
elif args.prefix:
|
||||
asset = args.asset or PREFIX_ASSET.get(args.prefix, f"/ASSETS/P{args.prefix[0]}")
|
||||
label = f"C-{args.prefix}11"
|
||||
for p, a, l in COLUMNS:
|
||||
if p == args.prefix:
|
||||
label = l
|
||||
break
|
||||
run_column(args.prefix, asset, label)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user