Files
HC900-Crawler/scripts/analysis/gen_temp_profiles.py
windpacer 7b21c35af6 feat: 민감단온도 전환복귀제어 + SteamAdvisor + FeedRamp 전면 구현
=== 민감단온도(T_C) 전환복귀제어 (작업플랜 구현) ===
- FeedforwardModels: TempLowLimit, TcReturnRebTarget/Band, TcReturnDeltaAdRef/Band 추가
- FeedforwardEngine: sigTLow (T_C 하한 트리거, -1e9=비활성) + 온도기반 복귀게이트(tcRecovered)
  -> Recovering→Returning 전이: mbRecovered(물질수지) OR tcRecovered(reb-A+ΔT+T_C)
- FeedRampCalculator: 하강 램프 전면 구현 (RateUpPerMin/RateDnPerMin 분리, θ_up/θ_dn 분기, floor clamp)
- FeedRampExecutorService: 하강 램프 step 방향 지원
- FeedforwardConfigStore: 신규 6개 컬럼 SELECT/INSERT/UPDATE
- Hc900DbContext: temp_low_limit, tc_return_reb_target/band, tc_return_delta_ad_ref/band
- FeedforwardController: API 노출 + feed-ramp start/cancel/status

=== SteamAdvisor ===
- SteamAdvisorController: steam map 로드/시각화/제품매칭/온도프로파일
- steam.js, steam.html: SteamAdvisor 전용 UI 패널

=== Feed Ramp 실행 ===
- FeedRampExecutorService: BG service (BackgroundService)
- FeedRampJobStore: in-memory job store
- FfTrackingStore: ramp tracking DB
- FeedforwardSupervisor/WriteGuard: SP 쓰기 advisory + rate-limit

=== 분석 스크립트 ===
- gen_temp_profiles.py: 컬럼 온도 프로파일 기준 산출 → c{prefix}_tempref.json
- export_plotdata.py: analysis 결과 plot data export
- gen_instrument_ranges.py: 계기 범위 생성
- c6111_extract.py: C-6111 추출/운전모드 분류
- run_column.py: 전체 분석 파이프라인

=== Web UI ===
- ff.js/ff.html/ff.css: 전환류 상태기계 UI, TagBrowser, config save
- fast.js: Fast 조작 패널
- trend.js, pb.js, llmchat.js: 각 패널 확장
2026-06-06 18:33:56 +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"c{prefix}_data.pkl")
if prefix == "61" 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": f"c{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"c{prefix}_tempref.json")
with open(out, "w") as f:
json.dump(ref, f, indent=2, ensure_ascii=False)
print(f" c{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 ["61", "62", "81", "91", "92", "101", "102"]
for p in prefixes:
build(p, args.stable_from, args.stable_to)
if __name__ == "__main__":
main()