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

86 lines
3.4 KiB
Python

"""
계기 EU range 룩업 생성 → instrument_ranges.json (오프라인 분석 전용).
소스 우선순위 (사용자 확정 2026-06-06):
1) realtime_table PV_HighRange/PV_LowRange — 컨트롤러 실측값(권위). online 컬럼(5·6·8차).
2) xlsx InstructionsDisplayNumber — Experion 포인트 설정. online 안 된 컬럼(9·10차=C4) fallback.
★ 이 JSON은 오프라인 분석(prodmap/export_model 등)의 스파이크 클린징 입력 전용.
C# 운영 코드(SteamAdvisor)는 절대 이걸 안 씀 — realtime range를 live로 직접 읽음.
교차검증(2026-06-06): realtime vs xlsx 23/32 일치. 8차는 xlsx가 미동기(1000 vs 실측 18000)
→ online 컬럼은 realtime이 권위, xlsx는 미연결 컬럼만.
"""
import json
import os
import openpyxl
import psycopg
BASE = os.path.dirname(os.path.abspath(__file__))
XLSX = os.path.join(BASE, "..", "..", "docs", "Sinam_Tag_all.xlsx")
OUT = os.path.join(BASE, "instrument_ranges.json")
DSN_IIOT = "host=localhost port=5432 dbname=iiot_platform user=postgres password=postgres"
DEFAULT_LO = -5.0 # xlsx fallback의 LowRange (realtime 실측 대부분 -5~0)
def realtime_ranges():
"""5·6·8차 등 online 컬럼의 실측 EU range (권위)."""
out = {}
with psycopg.connect(DSN_IIOT) as c:
cur = c.cursor()
cur.execute("""
SELECT split_part(tagname,'.',1) AS base,
max(CASE WHEN tagname LIKE '%PV_HighRange' THEN livevalue::float END) AS hi,
max(CASE WHEN tagname LIKE '%PV_LowRange' THEN livevalue::float END) AS lo
FROM hc900.realtime_table
WHERE tagname LIKE '%PV_%Range'
GROUP BY 1
""")
for base, hi, lo in cur.fetchall():
if hi is not None:
out[base] = {"lo": lo if lo is not None else DEFAULT_LO,
"hi": hi, "src": "realtime"}
return out
def xlsx_ranges():
"""InstructionsDisplayNumber = range hi (Experion 포인트 설정)."""
wb = openpyxl.load_workbook(XLSX, read_only=True, data_only=True)
ws = wb["Sheet1"]
rows = ws.iter_rows(values_only=True)
next(rows) # 머지셀 None 행
header = list(next(rows)) # 실제 헤더
ni = header.index("ItemName")
idn = header.index("InstructionsDisplayNumber")
out = {}
for r in rows:
nm, v = r[ni], r[idn]
if not nm or not isinstance(v, (int, float)) or isinstance(v, bool):
continue # 숫자(range hi)만 — 'default' 등 문자열 스킵
base = nm.split(".")[0]
if base not in out: # 첫 숫자값 채택
out[base] = float(v)
return out
def main():
rt = realtime_ranges()
xl = xlsx_ranges()
ranges = dict(rt) # realtime 우선
n_xlsx = 0
for base, hi in xl.items():
if base not in ranges: # online 안 된 컬럼만 xlsx fallback
ranges[base] = {"lo": DEFAULT_LO, "hi": hi, "src": "xlsx"}
n_xlsx += 1
with open(OUT, "w") as f:
json.dump(ranges, f, indent=2, sort_keys=True)
print(f"realtime(실측) {len(rt)}개 + xlsx(fallback) {n_xlsx}개 = 총 {len(ranges)}개 태그")
print(f"저장: {OUT}")
# 9·10차 샘플 확인
for b in ["FICQ-9201", "FICQ-9218", "FICQ-10201", "FICQ-10218"]:
print(f" {b}: {ranges.get(b)}")
if __name__ == "__main__":
main()