Files
HC900-Crawler/scripts/analysis/gen_temp_profiles.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

100 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
컬럼 온도 프로파일 기준 산출 → c{prefix}_tempref.json (이격 모니터 레이어①).
근거: 메모리 §17 — 같은 제품·진공이면 단별 온도(reb-A>T_B>T_C>T_D)가 부하무관 불변.
제품 식별 = 온도프로파일(reb_temp 클러스터). 진공 종속이라 vacuum 기준 동시 산출.
안정구간 PROD에서 제품별 단별 median/σ + 진공 median → 기준밴드(±2σ).
오퍼레이터 이격 모니터/품질 게이트의 reference.
"""
import argparse
import json
import os
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
BASE = os.path.dirname(os.path.abspath(__file__))
STAGES = ["reb_temp", "T_B", "T_C", "T_D"] # 보텀(리보일러) → 탑상
PROD_MIN = 50 # feed 하한
MIN_CLUSTER = 0.05 # 클러스터 최소 비중(5%)
def cluster_products(reb):
"""reb_temp 1D 클러스터 = 제품 식별. 인접 중심차 <2℃면 병합(과분할 방지)."""
reb = reb.reshape(-1, 1)
for k in (3, 2, 1):
km = KMeans(k, n_init=10, random_state=0).fit(reb)
cen = np.sort(km.cluster_centers_.ravel())
sizes = np.bincount(km.labels_, minlength=k) / len(reb)
if k == 1:
return km
if all(cen[i+1]-cen[i] >= 2.0 for i in range(k-1)) and sizes.min() >= MIN_CLUSTER:
return km
return KMeans(1, n_init=10, random_state=0).fit(reb)
def build(prefix, stable_from=None, stable_to=None):
pkl = os.path.join(BASE, f"{prefix}_data.pkl")
if prefix == "C-6111" and not os.path.exists(pkl):
pkl = os.path.join(BASE, "c6111_data.pkl")
if not os.path.exists(pkl):
print(f" [skip] {prefix}: {pkl} 없음")
return None
df = pd.read_pickle(pkl)
df = df[df["mode"] == "PROD"].copy()
df = df[(df["feed"] > PROD_MIN) & df[STAGES + ["vacuum"]].notna().all(axis=1)]
if stable_from:
df = df[df["dtat"] >= pd.Timestamp(stable_from)]
if stable_to:
df = df[df["dtat"] <= pd.Timestamp(stable_to)]
if len(df) < 200:
print(f" [skip] {prefix}: 안정 PROD 부족 ({len(df)}행)")
return None
km = cluster_products(df["reb_temp"].values)
df["prod"] = km.labels_
order = df.groupby("prod")["reb_temp"].median().sort_values().index # 저온→고온
products = []
for rank, pid in enumerate(order):
g = df[df["prod"] == pid]
stages = {s: {"median": round(float(g[s].median()), 2),
"std": round(float(g[s].std()), 2)} for s in STAGES}
products.append({
"label": f"P{rank}", # 추후 PM/PGMEA/EL 등 화학 라벨 매핑
"n_rows": len(g),
"span_AD": round(float((g["reb_temp"] - g["T_D"]).median()), 2),
"vacuum": {"median": round(float(g["vacuum"].median()), 2),
"std": round(float(g["vacuum"].std()), 2)},
"stages": stages,
})
ref = {"column": prefix, "stages_order": STAGES,
"n_products": len(products),
"period": f"{df['dtat'].min():%Y-%m-%d}~{df['dtat'].max():%Y-%m-%d}",
"products": products}
out = os.path.join(BASE, f"{prefix}_tempref.json")
with open(out, "w") as f:
json.dump(ref, f, indent=2, ensure_ascii=False)
print(f" {prefix}: 제품 {len(products)}", end="")
for p in products:
s = p["stages"]
print(f"[{p['label']} reb{s['reb_temp']['median']:.1f}/Tc{s['T_C']['median']:.1f}/"
f"Td{s['T_D']['median']:.1f} vac{p['vacuum']['median']:.0f} n{p['n_rows']}]", end=" ")
print(f"{os.path.basename(out)}")
return ref
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--prefix", help="단일 컬럼 (생략시 전체)")
ap.add_argument("--from", dest="stable_from", help="안정구간 시작 YYYY-MM-DD")
ap.add_argument("--to", dest="stable_to", help="안정구간 끝")
args = ap.parse_args()
prefixes = [args.prefix] if args.prefix else ["C-6111", "C-6211", "C-8111", "C-9111", "C-9211", "C-10111", "C-10211"]
for p in prefixes:
build(p, args.stable_from, args.stable_to)
if __name__ == "__main__":
main()