=== 컬럼명칭 통일 (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 환경변수 설정
184 lines
6.7 KiB
Python
184 lines
6.7 KiB
Python
"""
|
|
형제 컬럼 확장(작업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 = [
|
|
("51", "/ASSETS/P5", "C-5111 (5차)"), # 측류 솔벤트, T_C 센서 부재(대체)
|
|
("61", "/ASSETS/P6", "C-6111 (6-1차)"),
|
|
("62", "/ASSETS/P6", "C-6211 (6-2차)"),
|
|
("81", "/ASSETS/P8", "C-8111 (8차, 단일 train)"),
|
|
("91", "/ASSETS/P9", "C-9111 (9-1차)"),
|
|
("92", "/ASSETS/P9", "C-9211 (9-2차)"),
|
|
("101", "/ASSETS/P10", "C-10111 (10-1차) ※본 dump엔 시운전기간 多, 생산샘플 적음"),
|
|
("102", "/ASSETS/P10", "C-10211 (10-2차) ※첫생산 5/26~, 이후 데이터 유효"),
|
|
]
|
|
# ※ 10차는 본 dump 창(2026-02~06) 대부분이 시운전이라 PROD 샘플이 적음(10-1 2%, 10-2 5/26~).
|
|
# 단 2026-06 현재 정상 생산 진입 — 데이터 누적되면(또는 최신 dump 재반입) 정상 분석 대상.
|
|
# 현재는 생산구간만 추려 쓰되 샘플 부족에 주의(맵 R²·트리거 변동성 클 수 있음).
|
|
|
|
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}11_data.pkl 저장."""
|
|
from c6111_extract import roles_for, tag_frame, classify_phases, clip_to_ranges
|
|
|
|
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 = clip_to_ranges(df, roles) # 계기 EU range 밖 스파이크 → NaN
|
|
df["mode"] = classify_phases(df)
|
|
col_key = f"C-{prefix}11"
|
|
out = os.path.join(BASE, f"{col_key}_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)."""
|
|
col_key = f"C-{prefix}11"
|
|
data = os.path.join(BASE, f"{col_key}_data.pkl")
|
|
cmd = [PY, os.path.join(BASE, script), "--data", data, "--prefix", col_key]
|
|
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:
|
|
col_key = f"C-{prefix}11"
|
|
pkl = os.path.join(BASE, f"{col_key}_data.pkl")
|
|
# 6-1 legacy: c6111_data.pkl
|
|
if not os.path.exists(pkl):
|
|
alt = os.path.join(BASE, "c6111_data.pkl")
|
|
if prefix == "61" and 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()
|