컬럼명칭 통일 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 환경변수 설정
This commit is contained in:
76
docs/작업지시서-민감단제어-UI추가.md
Normal file
76
docs/작업지시서-민감단제어-UI추가.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 작업지시서 — 민감단(T_C) 전환·복귀 제어 UI 추가 (2026-06-06)
|
||||||
|
|
||||||
|
> **⚠️ 진단 결과 (diagnosis-checklist.md 기준, 2026-06-06)**
|
||||||
|
>
|
||||||
|
> ### 1. `MapConfig`에서 `TcReturnTcTarget`/`TcReturnTcBand` 누락 (MED)
|
||||||
|
>
|
||||||
|
> **문제**: `MapConfig` API 응답 매핑(`FeedforwardController.cs:199-233`)에서 `tcReturnTcTarget`, `tcReturnTcBand` 2개 누락. 나머지 5개는 매핑됨.
|
||||||
|
> **근거**: `FeedforwardController.cs:219-223` — `tcReturnRebTarget/Band`, `tcReturnDeltaAdRef/Band`, `tempLowLimit`은 포함되지만 `tcReturnTcTarget/Band` 없음.
|
||||||
|
> **영향**: UI 설정 폼 재로드 시 이 2개 필드의 값이 손실. (엔진 게이트는 ConfigStore→ColumnConfig에서 직접 읽으므로 게이트 동작에는 영향 없음.)
|
||||||
|
> **수정**: `MapConfig` 객체에 `tcReturnTcTarget`, `tcReturnTcBand` 2줄 추가.
|
||||||
|
>
|
||||||
|
> ### 2. 작업2 — `Mode`/`ModeReason` 이미 존재 (진단 오류 수정)
|
||||||
|
>
|
||||||
|
> `AdvisoryResult`에 `Mode`(FeedforwardModels.cs:131)와 `ModeReason`(:132)가 이미 존재하며 `MapColumn`에서 `mode`, `modeReason`으로 노출됨. 모드 배지(Normal/Recovering/Returning) + 사유 표시는 **지금 당장 구현 가능**. 실제로 없는 것은 `EnteredByTcLow`와 게이트 개별 진척(`rebInBand` 등)뿐이며, 이는 체크리스트(부가기능)에만 필요. 작업지시서가 이미 "advisory 응답에 Mode/사유 필드 있는지 확인, 없으면 확장 선행"이라 명시한 조건부 항목.
|
||||||
|
>
|
||||||
|
> ### 3. 작업3 — `StartFeedRamp` 업램프 전용 체크 (MED, 타당)
|
||||||
|
>
|
||||||
|
> **문제**: `FeedRampCalculator`는 하강을 완전 지원하지만, `StartFeedRamp` 핸들러(`FeedforwardController.cs:267-268`)가 `if (body.targetFeed <= adv.CurrentFeed) return BadRequest("업램프만 지원")`로 하강 차단.
|
||||||
|
> **영향**: 운전원이 하강 램프 시작 시 400 Bad Request.
|
||||||
|
> **수정**: 체크 제거 + `ff.js` 방향 자동 판정 + 확인 문구 방향별 변경.
|
||||||
|
>
|
||||||
|
> ### 요약
|
||||||
|
>
|
||||||
|
> | 작업 | 실제 상태 | 수정 필요 |
|
||||||
|
> |---|---|---|
|
||||||
|
> | 작업1 (설정 폼) | `MapConfig`에서 `TcReturnTcTarget/Band` 2줄 누락 | `FeedforwardController.cs` 2줄 추가 |
|
||||||
|
> | 작업2 (상태 감시) | `Mode`/`ModeReason` 이미 존재 → 모드 배지 즉시 구현 가능 | 게이트 체크리스트용 `EnteredByTcLow` 등 추가는 선택 |
|
||||||
|
> | 작업3 (하강 램프) | `StartFeedRamp` 업램프 전용 체크 존재 | `FeedforwardController.cs:267-268` 체크 제거 |
|
||||||
|
>
|
||||||
|
> ---
|
||||||
|
|
||||||
|
> 백엔드 완료(`작업플랜-민감단온도-전환복귀제어.md`): 엔진 상태기계·복귀 게이트 3버그 수정 + config 풀스택(DB/ConfigStore) 연결됨.
|
||||||
|
> 남은 것 = **운전원이 파라미터를 설정/감시하는 UI**. 대상 `wwwroot/panes/ff.html` · `wwwroot/js/ff.js`.
|
||||||
|
|
||||||
|
## 0. 현 상태
|
||||||
|
- 백엔드: `ColumnConfig`에 `TempLowLimit`·`TcReturnRebTarget/Band`·`TcReturnDeltaAdRef/Band`·`TcReturnTcTarget/Band` 존재. DB 왕복(`ff_column_config` 컬럼 + ConfigStore SELECT/INSERT/UPDATE) 완료.
|
||||||
|
- 엔진: `sigTLow`(T_C 하한 전환류 트리거) + 복귀 게이트(reb in-band & ΔT(A-D) 안정 & T_C in-band) + `EnteredByTcLow` 진입원인 분기 동작.
|
||||||
|
- UI: `ff-cfg-list`(설정 폼, ff.js 동적 렌더) + advisory 화면 존재. **새 필드·상태표시 미반영**.
|
||||||
|
|
||||||
|
## 작업 1 — 설정 폼에 민감단 파라미터 추가 [우선순위 1]
|
||||||
|
**대상**: `ff.js`의 `ffLoadConfig`/config 렌더(`ff-cfg-list`) + config 저장 API 바디.
|
||||||
|
**추가 입력 필드**(컬럼별):
|
||||||
|
- `TempLowLimit`(T_C 하한 트리거, ℃ raw. 비활성 -1e9)
|
||||||
|
- `TcReturnTcTarget`(T_C 목표온도 ℃, **reb-A와 다름**) · `TcReturnTcBand`(±폭, 기본 1.0)
|
||||||
|
- `TcReturnRebTarget`(reb-A 복귀목표) · `TcReturnRebBand`(기본 0.5)
|
||||||
|
- `TcReturnDeltaAdRef`(ΔT(A-D) 기준) · `TcReturnDeltaAdBand`(기본 0.4)
|
||||||
|
**검증**: 저장→재로드 시 값 유지(DB 왕복). NaN 필드는 빈칸 표시(비활성).
|
||||||
|
**주의**: ★`TcReturnTcTarget`은 T_C(민감단) 목표 — reb-A 목표와 혼동 금지(별 입력란·라벨 명확히). 미설정 시 T_C in-band 게이트 비활성(안전).
|
||||||
|
**제안 보조**: 온도프로파일 `c{prefix}_tempref.json`의 매칭제품 median을 target 기본값으로 자동제안 버튼(`/api/steam/tempprofile/{col}` 재활용).
|
||||||
|
|
||||||
|
## 작업 2 — 상태기계(ColumnMode) 감시 표시 [우선순위 2]
|
||||||
|
**대상**: ff advisory 화면 + `/api/ff/advisory` 응답(`AdvisoryResult.Mode` 등).
|
||||||
|
**표시**:
|
||||||
|
- 현재 모드 배지: **Normal / Recovering(전환류) / Returning(복귀램프)** — 색상 구분(정상 녹색·전환류 주황·복귀 파랑).
|
||||||
|
- 전환류 진입 시 사유(`SeverityText`: 온도LOW/온도HIGH/ΔP플러딩 등) + **진입원인이 T_C 하한인지**(`EnteredByTcLow`) 노출.
|
||||||
|
- 복귀 게이트 진척: reb in-band·ΔT(A-D) 안정·T_C in-band 각 충족 여부 체크리스트(운전원이 "왜 아직 복귀 안 하나" 파악).
|
||||||
|
**검증**: 모드 전이 시 배지·사유 실시간 갱신. (Mode/ModeReason 이미 advisory 응답에 존재.)
|
||||||
|
|
||||||
|
## 작업 3 — 하강 램프 UI [우선순위 3]
|
||||||
|
**대상**: `ff-ramp`(램프 계산기) + `/api/ff/feed-ramp`.
|
||||||
|
**현 상태**: `FeedRampCalculator`가 상승/하강 양방향 구현됨. `StartFeedRamp` 업램프 전용 체크 제거됨. UI는 상승 전제.
|
||||||
|
**추가**: targetFeed < currentFeed 입력 시 **하강 램프**로 자동 인식, 방향 배지(↑상승/↓하강) + 하강 rate 표시. 확인 다이얼로그 문구 "올립니다"→방향별("내립니다").
|
||||||
|
**검증**: 하강 타겟 입력 시 하강 시간/rate 산출, 방향 표기 정확.
|
||||||
|
|
||||||
|
## 작업 4 (선택) — T_C 유지 advisory 표시
|
||||||
|
**대상**: STEAM advisory(steam.js) 또는 ff 화면.
|
||||||
|
**표시**: 현재 T_C vs target 편차, 모듈1 제안 steam OP(디커플링), 편차 추세. (모듈1 백엔드 구현 후.)
|
||||||
|
|
||||||
|
## 권장순서 / 의존
|
||||||
|
작업1(설정) → 작업2(상태감시) → 작업3(하강램프) → 작업4(선택, 모듈1 의존).
|
||||||
|
작업2는 `Mode`/`ModeReason`이 이미 advisory 응답에 존재 → 모드 배지·사유 표시 즉시 구현 가능. 게이트 개별 진척 체크리스트는 `AdvisoryResult`에 `EnteredByTcLow`/`RebInBand`/`DeltaAdStable`/`TcInBand` 필드 추가 후 구현.
|
||||||
|
|
||||||
|
## 주의(공통)
|
||||||
|
- advisory-only·개별 follow 존중(자동 write 강제 금지).
|
||||||
|
- 9·10차(C4 미연결)는 설정·백테스트만, 라이브 상태표시는 6차(C3) 우선.
|
||||||
|
- T_C target은 제품전환(bimodal) 시 재설정 필요 — tempref 제품 선택과 연동.
|
||||||
361
docs/작업플랜-스팀컬럼명칭통일.md
Normal file
361
docs/작업플랜-스팀컬럼명칭통일.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# 작업플랜 — SteamAdvisor 컬럼명칭 통일 (2026-06-06)
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
1. `appsettings.json`에서 `c6111`/`c61` 중복 제거
|
||||||
|
2. 컬럼키·파일prefix 모두 `C-6111`, `C-6211`, `C-8111`, `C-9111`, `C-9211`, `C-10111`, `C-10211`으로 통일
|
||||||
|
3. 컬럼키 = 파일prefix → 매핑 헬퍼 불필요
|
||||||
|
|
||||||
|
## 통일 매핑
|
||||||
|
|
||||||
|
| 컬럼키 (UI/API) | 파일 prefix | 설명 |
|
||||||
|
|-----------------|-------------|------|
|
||||||
|
| `C-6111` | `C-6111` | 6-1차 |
|
||||||
|
| `C-6211` | `C-6211` | 6-2차 |
|
||||||
|
| `C-8111` | `C-8111` | 8차 |
|
||||||
|
| `C-9111` | `C-9111` | 9-1차 |
|
||||||
|
| `C-9211` | `C-9211` | 9-2차 |
|
||||||
|
| `C-10111` | `C-10111` | 10-1차 |
|
||||||
|
| `C-10211` | `C-10211` | 10-2차 |
|
||||||
|
|
||||||
|
**TagsFor numeric suffix**: 컬럼키에서 `"C-"` prefix 제거 → `"6111"`, `"6211"`, `"8111"` 등
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 0 — 파일 리네임 [선행 필수]
|
||||||
|
|
||||||
|
`scripts/analysis/` 내 데이터 파일 일괄 리네임:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts/analysis
|
||||||
|
|
||||||
|
# Model files
|
||||||
|
mv c6111_model.json C-6111_model.json
|
||||||
|
mv c61_model.json C-6111_model.json.bak # 중복, 제거
|
||||||
|
mv c62_model.json C-6211_model.json
|
||||||
|
mv c81_model.json C-8111_model.json
|
||||||
|
mv c91_model.json C-9111_model.json
|
||||||
|
mv c92_model.json C-9211_model.json
|
||||||
|
mv c101_model.json C-10111_model.json
|
||||||
|
mv c102_model.json C-10211_model.json
|
||||||
|
|
||||||
|
# Tempref files
|
||||||
|
mv c61_tempref.json C-6111_tempref.json
|
||||||
|
mv c62_tempref.json C-6211_tempref.json
|
||||||
|
mv c81_tempref.json C-8111_tempref.json
|
||||||
|
mv c91_tempref.json C-9111_tempref.json
|
||||||
|
mv c92_tempref.json C-9211_tempref.json
|
||||||
|
mv c101_tempref.json C-10111_tempref.json
|
||||||
|
mv c102_tempref.json C-10211_tempref.json
|
||||||
|
|
||||||
|
# Plotdata files
|
||||||
|
mv c6111_plotdata.json C-6111_plotdata.json
|
||||||
|
mv c61_plotdata.json C-6111_plotdata.json.bak # 중복, 제거
|
||||||
|
mv c62_plotdata.json C-6211_plotdata.json
|
||||||
|
mv c81_plotdata.json C-8111_plotdata.json
|
||||||
|
mv c91_plotdata.json C-9111_plotdata.json
|
||||||
|
mv c92_plotdata.json C-9211_plotdata.json
|
||||||
|
mv c101_plotdata.json C-10111_plotdata.json
|
||||||
|
mv c102_plotdata.json C-10211_plotdata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**동시 수정**: 각 JSON 파일 내부의 `"column"` / `"prefix"` 필드 값도 `C-6111` 등으로 변경.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON 내부 필드 수정 (python one-liner)
|
||||||
|
for f in C-*_model.json; do
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = json.load(open('$f'))
|
||||||
|
d['column'] = '$f'.replace('_model.json','')
|
||||||
|
json.dump(d, open('$f','w'), indent=2)
|
||||||
|
"
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in C-*_plotdata.json; do
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = json.load(open('$f'))
|
||||||
|
d['prefix'] = '$f'.replace('_plotdata.json','')
|
||||||
|
json.dump(d, open('$f','w'), indent=2)
|
||||||
|
"
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in C-*_tempref.json; do
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = json.load(open('$f'))
|
||||||
|
d['column'] = '$f'.replace('_tempref.json','')
|
||||||
|
json.dump(d, open('$f','w'), indent=2)
|
||||||
|
"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 1 — appsettings.json 수정
|
||||||
|
|
||||||
|
```json
|
||||||
|
"SteamAdvisor": {
|
||||||
|
"ModelPath": "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_model.json",
|
||||||
|
"PlotDataDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
||||||
|
"ModelDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
||||||
|
"DefaultColumn": "C-6111",
|
||||||
|
"Columns": {
|
||||||
|
"C-6111": { "Feed": "FICQ-6101.PV", "Product": "FICQ-6118.PV", "TC": "TI-6111C", "SteamOp": "TICA-6111A.OP", "SteamFlow": "FIQ-6115" },
|
||||||
|
"C-6211": { "Feed": "FICQ-6201.PV", "Product": "FICQ-6218.PV", "TC": "TI-6211C", "SteamOp": "TICA-6211A.OP", "SteamFlow": "FIQ-6215" },
|
||||||
|
"C-8111": { "Feed": "FICQ-8101.PV", "Product": "FICQ-8118.PV", "TC": "TI-8111C", "SteamOp": "TICA-8111A.OP", "SteamFlow": "FIQ-8115" },
|
||||||
|
"C-9111": { "Feed": "FICQ-9101.PV", "Product": "FICQ-9118.PV", "TC": "TI-9111C", "SteamOp": "TICA-9111A.OP", "SteamFlow": "FIQ-9115" },
|
||||||
|
"C-9211": { "Feed": "FICQ-9201.PV", "Product": "FICQ-9218.PV", "TC": "TI-9211C", "SteamOp": "TICA-9211A.OP", "SteamFlow": "FIQ-9215" },
|
||||||
|
"C-10111": { "Feed": "FICQ-10101.PV", "Product": "FICQ-10118.PV", "TC": "TI-10111C", "SteamOp": "TICA-10111A.OP", "SteamFlow": "FIQ-10115" },
|
||||||
|
"C-10211": { "Feed": "FICQ-10201.PV", "Product": "FICQ-10218.PV", "TC": "TI-10211C", "SteamOp": "TICA-10211A.OP", "SteamFlow": "FIQ-10215" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 2 — SteamAdvisorController.cs 수정
|
||||||
|
|
||||||
|
### 2-1. 헬퍼 메서드 추가
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 컬럼키 "C-6111" → 파일prefix "C-6111" (동일)
|
||||||
|
// 컬럼키 "C-6111" → TagsFor numeric suffix "6111"
|
||||||
|
private static string ToSuffix(string col) => col.StartsWith("C-") ? col[2..] : col;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-2. `ListModels` — 파일명에서 컬럼키 추출 (변경 불필요)
|
||||||
|
|
||||||
|
`*_model.json` 파일명의 `_model.json` 제거 → `C-6111` 반환. 컬럼키 = 파일prefix이므로 추가 변환 불필요.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전
|
||||||
|
.Select(n => n!.Replace("_model", "")) // "c6111"
|
||||||
|
|
||||||
|
// 변경 후 — 그대로 "C-6111" 반환
|
||||||
|
.Select(n => n!.Replace("_model", ""))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-3. `Backtest` — 컬럼키 그대로 파일prefix 사용
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전
|
||||||
|
var path = Path.Combine(plotDir, $"{col}_plotdata.json");
|
||||||
|
|
||||||
|
// 변경 후 — 컬럼키 = 파일prefix
|
||||||
|
var path = Path.Combine(plotDir, $"{col}_plotdata.json"); // 동일!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-4. `TempProfile` — 컬럼키 = 파일prefix, TagsFor는 numeric suffix
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전
|
||||||
|
var path = Path.Combine(dir, $"c{col}_tempref.json"); // col = "61"
|
||||||
|
var tagMap = TagsFor(col);
|
||||||
|
|
||||||
|
// 변경 후
|
||||||
|
var path = Path.Combine(dir, $"{col}_tempref.json"); // col = "C-6111"
|
||||||
|
var tagMap = TagsFor(ToSuffix(col)); // "6111"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-5. `TagsFor` — numeric suffix 기반 태그명 생성
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전 — col = "61", "62", "81" 등
|
||||||
|
private static Dictionary<string, string> TagsFor(string p)
|
||||||
|
{
|
||||||
|
var m = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["reb_temp"] = $"TICA-{p}11A.PV",
|
||||||
|
["T_B"] = $"TI-{p}11B.PV",
|
||||||
|
["T_C"] = $"TI-{p}11C.PV",
|
||||||
|
["T_D"] = $"TI-{p}11D.PV",
|
||||||
|
["vacuum"] = $"PICA-{p}11.PV",
|
||||||
|
};
|
||||||
|
switch (p) {
|
||||||
|
case "51": m["T_C"] = "TI-5111B.PV"; break;
|
||||||
|
case "81": m["reb_temp"] = "TICA-8111.PV"; m["vacuum"] = "PICA-8111A.PV"; break;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변경 후 — p = "6111", "6211", "8111" 등
|
||||||
|
private static Dictionary<string, string> TagsFor(string p)
|
||||||
|
{
|
||||||
|
var m = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["reb_temp"] = $"TICA-{p}A.PV",
|
||||||
|
["T_B"] = $"TI-{p}B.PV",
|
||||||
|
["T_C"] = $"TI-{p}C.PV",
|
||||||
|
["T_D"] = $"TI-{p}D.PV",
|
||||||
|
["vacuum"] = $"PICA-{p}.PV",
|
||||||
|
};
|
||||||
|
switch (p) {
|
||||||
|
case "8111": m["reb_temp"] = "TICA-8111.PV"; m["vacuum"] = "PICA-8111A.PV"; break;
|
||||||
|
case "9111": m["vacuum"] = "PICA-9111A.PV"; break;
|
||||||
|
case "9211": m["vacuum"] = "PICA-9211A.PV"; break;
|
||||||
|
case "10111": m["vacuum"] = "PICA-10111A.PV"; break;
|
||||||
|
case "10211": m["vacuum"] = "PICA-10211A.PV"; break;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-6. `Live` — 컬럼키 fallback 변경
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전
|
||||||
|
col ??= _config.GetValue<string>("SteamAdvisor:DefaultColumn") ?? "c6111";
|
||||||
|
|
||||||
|
// 변경 후
|
||||||
|
col ??= _config.GetValue<string>("SteamAdvisor:DefaultColumn") ?? "C-6111";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 3 — SteamAdvisor.cs 수정
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 변경 전
|
||||||
|
_modelPath = config.GetValue<string>("SteamAdvisor:ModelPath")
|
||||||
|
?? "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_model.json";
|
||||||
|
|
||||||
|
// 변경 후
|
||||||
|
_modelPath = config.GetValue<string>("SteamAdvisor:ModelPath")
|
||||||
|
?? "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_model.json";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 4 — UI (steam.js) 수정
|
||||||
|
|
||||||
|
### 4-1. `ST_TEMP_COLS` — 컬럼키 사용
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 변경 전
|
||||||
|
const ST_TEMP_COLS = [['61','6-1차'],['62','6-2차'],['81','8차'],['91','9-1차'],['92','9-2차'],['101','10-1차'],['102','10-2차']];
|
||||||
|
|
||||||
|
// 변경 후
|
||||||
|
const ST_TEMP_COLS = [
|
||||||
|
['C-6111','6-1차'],['C-6211','6-2차'],['C-8111','8차'],
|
||||||
|
['C-9111','9-1차'],['C-9211','9-2차'],['C-10111','10-1차'],['C-10211','10-2차']
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-2. API 호출 — 컬럼키 그대로 전달
|
||||||
|
|
||||||
|
`stLiveTick`, `stTempTick` 모두 컬럼키를 API에 전달. 백엔드가 컬럼키를 직접 사용하므로 추가 변환 불필요.
|
||||||
|
|
||||||
|
### 4-3. `stLoadColumns` — 기본 선택 컬럼 강제 [★ 누락 A 해결]
|
||||||
|
|
||||||
|
알파벳순 정렬 시 `C-10111`이 `C-6111`보다 앞서 첫 옵션 자동 선택됨 → 데이터 없는 10차 → `missing_tags` → 불로딩 재현.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 변경 전
|
||||||
|
[sel1, sel2].forEach(sel => {
|
||||||
|
sel.innerHTML = cols.map(c => `<option value="${c}">${c}</option>`).join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 변경 후 — DefaultColumn(C-6111)을 기본 선택으로 강제
|
||||||
|
const defaultCol = 'C-6111'; // 또는 d.defaultColumn에서 동적 획득
|
||||||
|
[sel1, sel2].forEach(sel => {
|
||||||
|
sel.innerHTML = cols.map(c => `<option value="${c}" ${c===defaultCol?'selected':''}>${c}</option>`).join('');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-3. `stLoadColumns` — 기본 선택 컬럼 강제 [★ 누락 A 해결]
|
||||||
|
|
||||||
|
알파벳순 정렬 시 `C-10111`이 `C-6111`보다 앞서 첫 옵션 자동 선택됨 → 데이터 없는 10차 → `missing_tags` → 불로딩 재현.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 변경 전
|
||||||
|
[sel1, sel2].forEach(sel => {
|
||||||
|
sel.innerHTML = cols.map(c => `<option value="${c}">${c}</option>`).join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 변경 후 — DefaultColumn(C-6111)을 기본 선택으로 강제
|
||||||
|
const defaultCol = 'C-6111'; // 또는 d.defaultColumn에서 동적 획득
|
||||||
|
[sel1, sel2].forEach(sel => {
|
||||||
|
sel.innerHTML = cols.map(c => `<option value="${c}" ${c===defaultCol?'selected':''}>${c}</option>`).join('');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 5 — Python 스크립트 출력 파일명 수정
|
||||||
|
|
||||||
|
Python 스크립트명 변경 불가 (`-` 포함 모듈 import 불가)이나, **출력 파일명**은 `C-6111` 형식으로 변경.
|
||||||
|
|
||||||
|
수정 대상 (각 스크립트의 `--prefix` 기본값 + 출력 파일명):
|
||||||
|
|
||||||
|
| 스크립트 | 변경 항목 |
|
||||||
|
|----------|----------|
|
||||||
|
| `c6111_extract.py` | `--data` 기본값: `c6111_data.pkl` → `C-6111_data.pkl` |
|
||||||
|
| `c6111_prodmap.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_shadow.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_rolling.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_startup.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_shutdown.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_operator_assist.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `c6111_export_model.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `gen_temp_profiles.py` | `--prefix` 기본값: `c61` → `C-6111` |
|
||||||
|
| `export_plotdata.py` | `--prefix` 기본값: `c6111` → `C-6111` |
|
||||||
|
| `run_column.py` | 스크립트 목록 + 데이터 파일명 |
|
||||||
|
|
||||||
|
**주의**: `c6111_data.pkl` → `C-6111_data.pkl`로 리네임 필요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 권장 순서
|
||||||
|
|
||||||
|
```
|
||||||
|
작업0 (파일 리네임) → 작업1 (appsettings) → 작업2 (Controller) → 작업3 (SteamAdvisor) → 작업4 (UI) → 작업5 (Python)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 리스크
|
||||||
|
|
||||||
|
| 항목 | 수준 | 대응 |
|
||||||
|
|------|------|------|
|
||||||
|
| 파일 리네임 누락 | 중 | `ls scripts/analysis/`로 확인 |
|
||||||
|
| `TagsFor` switch case 누락 | 중 | 기존 6개 case → 5개 case로 변경 (51 제거) |
|
||||||
|
| Python 스크립트 `--prefix` 기본값 누락 | 낮 | 스크립트 11개 확인 |
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
1. `ls scripts/analysis/` — 모든 데이터 파일이 `C-6111_` 등으로 시작
|
||||||
|
2. `dotnet build` 성공
|
||||||
|
3. `/api/steam/models` → `["C-6111", "C-6211", ...]` 반환 (중복 없음)
|
||||||
|
4. `/api/steam/live?col=C-6111` → 정상 응답
|
||||||
|
5. `/api/steam/tempprofile/C-6111` → 정상 응답
|
||||||
|
6. `/api/steam/backtest/C-6111` → 정상 응답
|
||||||
|
7. UI 컬럼 선택 → 차트 데이터 표시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 재진단 보완 (2026-06-06) — 통일 작업플랜의 누락 2건
|
||||||
|
|
||||||
|
> 명칭 통일 방향은 타당하나, 이 작업만으로 **원래 증상(라이브 불로딩)이 해결되지 않으며** Python 영향 범위가 과소평가됨.
|
||||||
|
|
||||||
|
### 🔴 누락 A — 통일해도 라이브 불로딩이 재현 (기본선택 강제 누락)
|
||||||
|
- 이 문서 본진단의 증상 = "라이브 차트 불로딩", 실제 원인 = **UI 기본 선택 컬럼이 알파벳순 첫 = 10차**(재평가 §실제 원인).
|
||||||
|
- 통일 후에도 `OrderBy(x)` 알파벳순 첫은 **`C-10111`**(`1 < 6`) → `st-col` 첫 옵션 자동선택 = 데이터 없는 10차 → `missing_tags` → **불로딩 그대로 재현**.
|
||||||
|
- `DefaultColumn=C-6111`(작업1)은 **Live API의 `col` 누락 시 fallback**일 뿐, **UI `select` 기본선택과 별개**(`steam.js stLoadColumns`가 첫 옵션 선택).
|
||||||
|
- **추가 작업 필요(작업4에 포함)**: `stLoadColumns`에서 **기본 선택을 `DefaultColumn`(C-6111)으로 강제** + `missing_tags` 사용자 표시. ★명칭 통일이 불로딩을 고친다는 착시 주의 — 별개 수정.
|
||||||
|
|
||||||
|
### 🟠 누락 B — Python `c{prefix}` 접두 패턴 (작업5 과소평가)
|
||||||
|
- 분석 스크립트·`gen_temp_profiles.py`가 파일명을 `f"c{prefix}_data.pkl"`처럼 **코드에 접두 `c`를 박아** 생성. prefix=`C-6111`이면 `"cC-6111_data.pkl"`로 **깨짐**.
|
||||||
|
- 작업5는 "`--prefix` 기본값 + 출력 파일명"만 명시 — 실제론 **각 스크립트 내부 `c{prefix}` → `{prefix}` 문자열 패턴**을 모두 바꿔야 함.
|
||||||
|
- `gen_temp_profiles.build()`의 6-1 fallback(`if prefix=="61"`)·`c6111_data.pkl` 경로 등도 함께 수정 대상.
|
||||||
|
- **작업5 보강**: "출력 파일명 변경" = 스크립트 내부 `c{prefix}` 접두 제거 + prefix 비교 리터럴(`"61"`,`"81"` 등) 갱신.
|
||||||
|
|
||||||
|
### 🟡 경미 (인지됨)
|
||||||
|
- 모듈명(`c6111_extract.py`, `-` import 불가로 변경 불가) vs 출력 prefix(`C-6111`) **영구 불일치** — 동작엔 무해하나 혼란 잔존. 헤더 주석으로 명시 권장.
|
||||||
|
|
||||||
|
### 종합
|
||||||
|
| 항목 | 통일 작업플랜 | 보완 |
|
||||||
|
|---|---|---|
|
||||||
|
| 명칭 혼재(c6111/c61 중복·3규약) | ✅ 해결 | 타당 |
|
||||||
|
| **라이브 불로딩(원래 증상)** | ❌ **미해결** | 작업4에 **기본선택=C-6111 강제 + missing_tags 표시** 추가 필수 |
|
||||||
|
| Python 파일명 | ⚠️ 과소평가 | 작업5를 **내부 `c{prefix}`→`{prefix}` 패턴 변경**으로 확장 |
|
||||||
153
docs/진단-스팀Advisory-라이브차트.md
Normal file
153
docs/진단-스팀Advisory-라이브차트.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# 진단 — Steam Advisory 라이브 차트 데이터 불로딩 (2026-06-06)
|
||||||
|
|
||||||
|
## 증상
|
||||||
|
|
||||||
|
Steam Advisory 탭 → "라이브" 패널에서 "조회" 버튼 클릭 후 차트와 입력값이 표시되지 않음.
|
||||||
|
|
||||||
|
## 진단 결과
|
||||||
|
|
||||||
|
### 🔴 HIGH — `ToDictionaryAsync` 중복 tagname `ArgumentException`
|
||||||
|
|
||||||
|
**문제**: `SteamAdvisorController.Live`와 `TempProfile` API에서 `_ctx.RealtimePoints.Where(...).ToDictionaryAsync(r => r.TagName, ...)` 사용. `realtime_table`의 UNIQUE 제약이 `(controller_id, tagname)`이므로 같은 `tagname`이 여러 `controller_id`로 존재할 수 있음. `ToDictionaryAsync`는 중복 키에서 `ArgumentException`을 던짐.
|
||||||
|
|
||||||
|
**근거**:
|
||||||
|
- `SteamAdvisorController.cs:133-135` — `ToDictionaryAsync(r => r.TagName, r => r.LiveValue)`
|
||||||
|
- `SteamAdvisorController.cs:194-196` — 동일 패턴
|
||||||
|
- `Hc900Controllers.cs:279-281` — 동일 패턴
|
||||||
|
- `Hc900DbContext.cs:488-498` — UNIQUE 제약이 `(controller_id, tagname)`
|
||||||
|
|
||||||
|
**영향**: API 호출 시 `ArgumentException` 발생 → 500 Internal Server Error → JS `catch (_) {}`로 삼켜짐 → 사용자에게 아무 표시도 없음.
|
||||||
|
|
||||||
|
**수정**: `GroupBy(r => r.TagName).ToDictionaryAsync(g => g.Key, g => g.OrderByDescending(r => r.Timestamp).FirstOrDefault()?.LiveValue)`로 변경. 최신 타임스탬프의 값을 사용.
|
||||||
|
|
||||||
|
### 🟠 MED — JS `catch (_) {}`로 예외 삼킴
|
||||||
|
|
||||||
|
**문제**: `stLiveTick`의 `catch (_) {}`가 API 에러를 완전히 무시. `missing_tags` 응답도 `stUpdateLive`에 전달되어 `d.recOp`가 `undefined`인 상태로 UI 갱신 시도.
|
||||||
|
|
||||||
|
**근거**: `steam.js:163-169` — `catch (_) {}`
|
||||||
|
|
||||||
|
**영향**: API 에러가 발생해도 사용자에게 표시되지 않음. 디버깅 불가.
|
||||||
|
|
||||||
|
**수정**: `catch (e)`로 변경 후 `st-live-msg`에 에러 메시지 표시. `missing_tags` 응답은 별도 처리.
|
||||||
|
|
||||||
|
### 🟡 LOW — register-map에 없는 태그 존재
|
||||||
|
|
||||||
|
**문제**: `appsettings.json`의 SteamAdvisor 태그 중 9차, 9-2차, 10-1차, 10-2차 관련 태그가 `register-map.json`에 없음.
|
||||||
|
|
||||||
|
**근거**: `appsettings.json:76-80` — 9차/10차 태그 정의. `register-map.json`에 해당 태그 없음.
|
||||||
|
|
||||||
|
**영향**: 해당 컬럼 선택 시 `missing_tags` 응답. 6차(C3)는 정상 동작.
|
||||||
|
|
||||||
|
**수정**: register-map에 태그 추가 또는 appsettings에서 제거.
|
||||||
|
|
||||||
|
## 교차 검증
|
||||||
|
|
||||||
|
| 질문 | 확인 방법 | 결과 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| Q1. 이미 수정된 문제인가? | 파일 현재 상태 확인 | ❌ 수정되지 않음 |
|
||||||
|
| Q2. 다른 레이어에서 처리되고 있는가? | 호출 계층 확인 | ❌ JS `catch (_) {}`로 삼킴 |
|
||||||
|
| Q3. 의도적 설계인가? | 문서·주석 확인 | ❌ 의도적 아님 |
|
||||||
|
| Q4. 실제 장애 시나리오가 있는가? | 재현 경로 구체화 | ✅ 같은 tagname이 여러 controller_id로 존재하면 500 Error |
|
||||||
|
|
||||||
|
## 수정 내역
|
||||||
|
|
||||||
|
| 파일 | 변경 내용 |
|
||||||
|
|------|-----------|
|
||||||
|
| `SteamAdvisorController.cs:133-136` | `ToDictionaryAsync` → `GroupBy` + `FirstOrDefault` |
|
||||||
|
| `SteamAdvisorController.cs:194-197` | 동일 패턴 수정 |
|
||||||
|
| `Hc900Controllers.cs:279-282` | 동일 패턴 수정 |
|
||||||
|
| `steam.js:163-176` | `catch (_) {}` → 에러 표시 + `missing_tags` 처리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 재평가 (2026-06-06, 코드·DB 데이터 검증 후)
|
||||||
|
|
||||||
|
진단을 데이터로 검증한 결과, **#1의 "현재 장애 원인" 진술은 부정확하나, 위험의 본질은 설계상 유효**함. 정정·심화:
|
||||||
|
|
||||||
|
### #1 재평가 — "현재 ArgumentException 발생"은 거짓, 그러나 위험은 실재
|
||||||
|
- **현 데이터**: `realtime_table`에 `tagname` **단독 UNIQUE 인덱스**(`realtime_table_tagname_key`)가 존재 → tagname 전역 유일 → 현재 중복 0행 → `ToDictionaryAsync` **지금은 안 터짐**. 즉 **현재 불로딩의 원인이 아님**.
|
||||||
|
- **그러나(핵심)**: HC900은 컨트롤러 간 **peer 통신으로 동일 태그를 미러링**함. 설계상 같은 tagname이 **여러 `controller_id`로 존재 가능**하며, 올바른 제약은 `UNIQUE(controller_id, tagname)`(메모리·`idx_realtime_table_ctrl_tag_unique`와 일치).
|
||||||
|
- **진짜 버그**: 현재 공존하는 **`tagname` 단독 UNIQUE 제약이 설계 위반**. C4 online 시 peer 미러 태그의 두 번째 INSERT/upsert를 막아 **적재 자체가 깨짐**.
|
||||||
|
- **결론**: `ToDictionaryAsync → GroupBy` 수정은 **단독 UNIQUE 제거를 전제로 방어적으로 정당**. 단 "현재 500 에러 발생"은 아님(잠재 위험).
|
||||||
|
|
||||||
|
### 추가로 필요한 근본 수정 (진단보다 한 겹 깊음)
|
||||||
|
1. **`realtime_table_tagname_key`(tagname 단독 UNIQUE) 제거** — 멀티컨트롤러 peer 적재 정합. `(controller_id, tagname)`만 유지.
|
||||||
|
2. **`Live`/`TempProfile` 조회를 `controller_id`로 한정** — 컬럼→컨트롤러 매핑(6차=C3 등)으로 필터. tagname만으로 조회하면 peer 미러된 동명 태그의 **엉뚱한 컨트롤러 값**이 섞일 수 있음(GroupBy로 예외는 막아도 *어느 컨트롤러 값인지* 모호).
|
||||||
|
|
||||||
|
### 실제 현재 불로딩 원인
|
||||||
|
- 6차(`C-6111`) 필수 태그(`FICQ-6101.PV`·`FICQ-6118.PV`·`TI-6111C`)는 realtime에 **모두 존재**(C3) → 6차 선택 시 정상 동작해야 함.
|
||||||
|
- 표면 트리거 = **기본 선택 컬럼이 `configured` 알파벳순 첫(`C-10111`=10차)** → 조회 시 C4 태그 부재 → `missing_tags` → `catch(_){}`(#2)로 삼켜져 무표시.
|
||||||
|
- **해법**: `DefaultColumn=C-6111`로 변경 + **UI `stLoadColumns`에서 기본 선택을 `C-6111`으로 강제**(작업플랜 4-3) + `missing_tags` 사용자 표시(#2 수정으로 반영됨). #3(9·10차 태그 부재)은 사실.
|
||||||
|
|
||||||
|
### 종합
|
||||||
|
| 항목 | 진단 주장 | 재평가 |
|
||||||
|
|---|---|---|
|
||||||
|
| #1 ToDictionaryAsync | HIGH·현재 500 | **현재는 미발생**(tagname 단독 UNIQUE)이나 **peer 설계상 잠재 실재** → GroupBy 정당 + **단독 UNIQUE 제거·controller_id 한정 조회**가 근본 |
|
||||||
|
| #2 catch 삼킴 | MED | 타당(표면 원인을 가림) |
|
||||||
|
| #3 9·10차 태그 부재 | LOW | 타당 + **기본 컬럼이 10차라 첫 화면 불로딩 트리거** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 추가 진단 — 컬럼명칭 혼재 문제 (2026-06-06)
|
||||||
|
|
||||||
|
### 🟠 MED — `c6111`/`c61` 중복 + 3가지 네이밍 체계 혼재
|
||||||
|
|
||||||
|
**문제**: `appsettings.json`에서 `c6111`과 `c61`이 같은 태그 매핑을 가리킴. `ListModels` API는 둘 다 반환 → UI에서 중복 선택 가능. 또한 컬럼키(`c6111`), 파일prefix(`c6111`/`c61`), TempProfile route param(`61`)이 각각 다른 규칙 사용.
|
||||||
|
|
||||||
|
**근거**: `appsettings.json:73-74` — `c6111`과 `c61` 중복. `SteamAdvisorController.cs:184` — `c{col}_tempref.json` (col = numeric suffix). `steam.js:74` — `ST_TEMP_COLS`가 numeric suffix 사용.
|
||||||
|
|
||||||
|
**영향**: UI 컬럼 선택에서 중복, TempProfile API 호출 시 컬럼키 ↔ numeric suffix 변환 누락 가능성.
|
||||||
|
|
||||||
|
**해결**: `작업플랜-스팀컬럼명칭통일.md` 참조. 컬럼키·파일prefix 모두 `C-6111` 형식으로 통일.
|
||||||
|
|
||||||
|
### 🟡 LOW — TagsFor numeric suffix 불일치
|
||||||
|
|
||||||
|
**문제**: `TagsFor`가 `"61"`, `"62"` 등 2자리 numeric suffix를 기대하지만, 컬럼명칭 통일 후 `"6111"`, `"6211"` 등 4자리로 변경됨. 템플릿 `$"TICA-{p}11A.PV"` → `$"TICA-{p}A.PV"`로 변경 필요.
|
||||||
|
|
||||||
|
**근거**: `SteamAdvisorController.cs:249-268` — `TagsFor` 메서드.
|
||||||
|
|
||||||
|
**영향**: 컬럼명칭 통일 후 수정하지 않으면 태그명 생성 실패 → `missing_tags` 응답.
|
||||||
|
|
||||||
|
**해결**: `작업플랜-스팀컬럼명칭통일.md` 작업 2-5 참조.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 통일 작업플랜 보완 진단 (2026-06-06)
|
||||||
|
|
||||||
|
> `작업플랜-스팀컬럼명칭통일.md`에 명칭 통일 방향은 타당하나, **원래 증상(라이브 불로딩) 해결과 Python 영향 범위가 과소평가**됨.
|
||||||
|
|
||||||
|
### 🔴 누락 A — 통일해도 라이브 불로딩 재현 (UI 기본선택 강제 누락)
|
||||||
|
|
||||||
|
- 통일 후에도 `OrderBy(x)` 알파벳순 첫은 **`C-10111`**(`1 < 6`) → `st-col` 첫 옵션 자동선택 = 데이터 없는 10차 → `missing_tags` → **불로딩 그대로 재현**.
|
||||||
|
- `DefaultColumn=C-6111`(작업1)은 **Live API의 `col` 누락 시 fallback**일 뿐, **UI `select` 기본선택과 별개**(`steam.js stLoadColumns`가 첫 옵션 선택).
|
||||||
|
- **해결**: `stLoadColumns`에서 기본 선택을 `C-6111`으로 강제 (작업플랜 4-3).
|
||||||
|
|
||||||
|
### 🟠 누락 B — Python `c{prefix}` 접두 패턴 (작업5 과소평가)
|
||||||
|
|
||||||
|
- `run_column.py:51` — `f"c{prefix}_data.pkl"`, `gen_temp_profiles.py:38` — `f"c{prefix}_data.pkl"`, `gen_temp_profiles.py:71` — `f"c{prefix}"`, `gen_temp_profiles.py:75` — `f"c{prefix}_tempref.json"`.
|
||||||
|
- prefix=`C-6111`이면 `"cC-6111_data.pkl"`로 **깨짐**.
|
||||||
|
- **해결**: 스크립트 내부 `c{prefix}` → `{prefix}` 패턴 변경 + `gen_temp_profiles.py:71`의 `"column": f"c{prefix}"` → `"column": prefix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 추가 진단 — 컬럼명칭 혼재 문제 (2026-06-06)
|
||||||
|
|
||||||
|
### 🟠 MED — `c6111`/`c61` 중복 + 3가지 네이밍 체계 혼재
|
||||||
|
|
||||||
|
**문제**: `appsettings.json`에서 `c6111`과 `c61`이 같은 태그 매핑을 가리킴. `ListModels` API는 둘 다 반환 → UI에서 중복 선택 가능. 또한 컬럼키(`c6111`), 파일prefix(`c6111`/`c61`), TempProfile route param(`61`)이 각각 다른 규칙 사용.
|
||||||
|
|
||||||
|
**근거**: `appsettings.json:73-74` — `c6111`과 `c61` 중복. `SteamAdvisorController.cs:184` — `c{col}_tempref.json` (col = numeric suffix). `steam.js:74` — `ST_TEMP_COLS`가 numeric suffix 사용.
|
||||||
|
|
||||||
|
**영향**: UI 컬럼 선택에서 중복, TempProfile API 호출 시 컬럼키 ↔ numeric suffix 변환 누락 가능성.
|
||||||
|
|
||||||
|
**해결**: `작업플랜-스팀컬럼명칭통일.md` 참조. 컬럼키·파일prefix 모두 `C-6111` 형식으로 통일.
|
||||||
|
|
||||||
|
### 🟡 LOW — TagsFor numeric suffix 불일치
|
||||||
|
|
||||||
|
**문제**: `TagsFor`가 `"61"`, `"62"` 등 2자리 numeric suffix를 기대하지만, 컬럼명칭 통일 후 `"6111"`, `"6211"` 등 4자리로 변경됨. 템플릿 `$"TICA-{p}11A.PV"` → `$"TICA-{p}A.PV"`로 변경 필요.
|
||||||
|
|
||||||
|
**근거**: `SteamAdvisorController.cs:249-268` — `TagsFor` 메서드.
|
||||||
|
|
||||||
|
**영향**: 컬럼명칭 통일 후 수정하지 않으면 태그명 생성 실패 → `missing_tags` 응답.
|
||||||
|
|
||||||
|
**해결**: `작업플랜-스팀컬럼명칭통일.md` 작업 2-5 참조.
|
||||||
2
industrial-comm/cpp/sigpipe_ignore.c
Normal file
2
industrial-comm/cpp/sigpipe_ignore.c
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include <signal.h>
|
||||||
|
static void __attribute__((constructor)) init(void) { signal(SIGPIPE, SIG_IGN); }
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <grpcpp/server.h>
|
#include <grpcpp/server.h>
|
||||||
#include <grpcpp/server_builder.h>
|
#include <grpcpp/server_builder.h>
|
||||||
#include <grpcpp/server_context.h>
|
#include <grpcpp/server_context.h>
|
||||||
@@ -374,6 +375,8 @@ int main(int argc, char* argv[])
|
|||||||
if (argc > 4) grpc_port = std::atoi(argv[4]);
|
if (argc > 4) grpc_port = std::atoi(argv[4]);
|
||||||
if (argc > 5) modbus_port = static_cast<uint16_t>(std::atoi(argv[5]));
|
if (argc > 5) modbus_port = static_cast<uint16_t>(std::atoi(argv[5]));
|
||||||
|
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
Logger::instance().set_file("/tmp/hc900_gateway.log");
|
Logger::instance().set_file("/tmp/hc900_gateway.log");
|
||||||
|
|
||||||
Hc900Gateway gateway(host, modbus_port, map_path, poll_ms, grpc_port);
|
Hc900Gateway gateway(host, modbus_port, map_path, poll_ms, grpc_port);
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ bool ModbusTCP::read_registers(std::uint16_t addr,
|
|||||||
req[10] = count >> 8;
|
req[10] = count >> 8;
|
||||||
req[11] = count & 0xFF;
|
req[11] = count & 0xFF;
|
||||||
|
|
||||||
if (::send(sock_, req, sizeof(req), 0) < 0) {
|
if (::send(sock_, req, sizeof(req), MSG_NOSIGNAL) < 0) {
|
||||||
last_error_ = TransportError::Disconnected;
|
last_error_ = TransportError::Disconnected;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ bool ModbusTCP::write_registers(std::uint16_t addr,
|
|||||||
req[14 + i * 2] = values[i] & 0xFF;
|
req[14 + i * 2] = values[i] & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (::send(sock_, req.data(), req.size(), 0) < 0) {
|
if (::send(sock_, req.data(), req.size(), MSG_NOSIGNAL) < 0) {
|
||||||
last_error_ = TransportError::Disconnected;
|
last_error_ = TransportError::Disconnected;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
34
scripts/analysis/C-10111_model.json
Normal file
34
scripts/analysis/C-10111_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-10111",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
-0.4608333188018109,
|
||||||
|
0.030778679890158908,
|
||||||
|
8.364320439105626
|
||||||
|
],
|
||||||
|
"intercept": 284.18717440136294,
|
||||||
|
"linear_r2": 0.2019,
|
||||||
|
"gbm_r2": 0.9981,
|
||||||
|
"valve_poly": [
|
||||||
|
-1.4012718976320648e-07,
|
||||||
|
0.0003106626129801823,
|
||||||
|
-0.18598847329591273,
|
||||||
|
85.81924723424272
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 693.5,
|
||||||
|
"product": 104.9,
|
||||||
|
"T_C": 58.8
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 1264.4,
|
||||||
|
"product": 1005.0,
|
||||||
|
"T_C": 81.8
|
||||||
|
},
|
||||||
|
"n_operating_points": 23,
|
||||||
|
"n_prod_rows": 5098
|
||||||
|
}
|
||||||
67
scripts/analysis/C-10111_tempref.json
Normal file
67
scripts/analysis/C-10111_tempref.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"column": "C-10111",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 2,
|
||||||
|
"period": "2026-02-20~2026-04-13",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 361,
|
||||||
|
"span_AD": 9.02,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 45.12,
|
||||||
|
"std": 13.38
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 78.78,
|
||||||
|
"std": 4.2
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 77.64,
|
||||||
|
"std": 11.64
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 71.75,
|
||||||
|
"std": 8.56
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 69.84,
|
||||||
|
"std": 8.77
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 4897,
|
||||||
|
"span_AD": 3.22,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 76.95,
|
||||||
|
"std": 3.59
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 84.3,
|
||||||
|
"std": 0.67
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 83.46,
|
||||||
|
"std": 3.33
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 81.71,
|
||||||
|
"std": 4.32
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 81.28,
|
||||||
|
"std": 7.14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-10211_model.json
Normal file
34
scripts/analysis/C-10211_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-10211",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.9294864374761924,
|
||||||
|
-0.15452592481820945,
|
||||||
|
-4.355733003639835
|
||||||
|
],
|
||||||
|
"intercept": 51.96343762286648,
|
||||||
|
"linear_r2": 0.2824,
|
||||||
|
"gbm_r2": 0.999,
|
||||||
|
"valve_poly": [
|
||||||
|
6.752259912738752e-08,
|
||||||
|
-0.00010864860285235597,
|
||||||
|
0.0801331606949025,
|
||||||
|
7.741006044043883
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 590.9,
|
||||||
|
"product": 271.8,
|
||||||
|
"T_C": 78.1
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 838.3,
|
||||||
|
"product": 760.9,
|
||||||
|
"T_C": 79.4
|
||||||
|
},
|
||||||
|
"n_operating_points": 30,
|
||||||
|
"n_prod_rows": 20701
|
||||||
|
}
|
||||||
67
scripts/analysis/C-10211_tempref.json
Normal file
67
scripts/analysis/C-10211_tempref.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"column": "C-10211",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 2,
|
||||||
|
"period": "2026-05-26~2026-06-02",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 19649,
|
||||||
|
"span_AD": 4.62,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 49.97,
|
||||||
|
"std": 0.22
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 82.45,
|
||||||
|
"std": 0.79
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 81.52,
|
||||||
|
"std": 0.87
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 78.91,
|
||||||
|
"std": 0.65
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 78.03,
|
||||||
|
"std": 0.97
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 1080,
|
||||||
|
"span_AD": 23.54,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 49.97,
|
||||||
|
"std": 0.16
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 87.58,
|
||||||
|
"std": 1.08
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 85.04,
|
||||||
|
"std": 1.58
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 78.36,
|
||||||
|
"std": 2.07
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 64.54,
|
||||||
|
"std": 4.55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-6111_model.json
Normal file
34
scripts/analysis/C-6111_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-6111",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.7327615829237054,
|
||||||
|
-0.02428538513646001,
|
||||||
|
7.063793833539203
|
||||||
|
],
|
||||||
|
"intercept": -585.4115834877662,
|
||||||
|
"linear_r2": 0.9861,
|
||||||
|
"gbm_r2": 0.9949,
|
||||||
|
"valve_poly": [
|
||||||
|
4.5825416007577506e-07,
|
||||||
|
-0.000721366513035835,
|
||||||
|
0.41181608994764535,
|
||||||
|
-42.70089479377073
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 380.5,
|
||||||
|
"product": 329.2,
|
||||||
|
"T_C": 83.9
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 911.8,
|
||||||
|
"product": 824.0,
|
||||||
|
"T_C": 86.1
|
||||||
|
},
|
||||||
|
"n_operating_points": 479,
|
||||||
|
"n_prod_rows": 333626
|
||||||
|
}
|
||||||
67
scripts/analysis/C-6111_tempref.json
Normal file
67
scripts/analysis/C-6111_tempref.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"column": "C-6111",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 2,
|
||||||
|
"period": "2026-02-05~2026-06-05",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 171623,
|
||||||
|
"span_AD": 1.69,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 112.99,
|
||||||
|
"std": 0.38
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 84.81,
|
||||||
|
"std": 0.5
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 84.33,
|
||||||
|
"std": 0.43
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 84.07,
|
||||||
|
"std": 0.28
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 83.12,
|
||||||
|
"std": 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 162003,
|
||||||
|
"span_AD": 4.56,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 113.01,
|
||||||
|
"std": 0.68
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 87.48,
|
||||||
|
"std": 0.69
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 86.54,
|
||||||
|
"std": 0.56
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 85.48,
|
||||||
|
"std": 0.39
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 82.92,
|
||||||
|
"std": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-6211_model.json
Normal file
34
scripts/analysis/C-6211_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-6211",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.4192774636696495,
|
||||||
|
-0.0031327650421361097,
|
||||||
|
56.90599492143593
|
||||||
|
],
|
||||||
|
"intercept": -4696.245060756706,
|
||||||
|
"linear_r2": 0.9965,
|
||||||
|
"gbm_r2": 0.998,
|
||||||
|
"valve_poly": [
|
||||||
|
3.2700894960484994e-07,
|
||||||
|
-0.0005053236912242627,
|
||||||
|
0.31737798932141487,
|
||||||
|
-3.9334436250422447
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 391.1,
|
||||||
|
"product": 355.8,
|
||||||
|
"T_C": 84.0
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 906.3,
|
||||||
|
"product": 823.4,
|
||||||
|
"T_C": 86.9
|
||||||
|
},
|
||||||
|
"n_operating_points": 479,
|
||||||
|
"n_prod_rows": 332544
|
||||||
|
}
|
||||||
67
scripts/analysis/C-6211_tempref.json
Normal file
67
scripts/analysis/C-6211_tempref.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"column": "C-6211",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 2,
|
||||||
|
"period": "2026-02-05~2026-06-05",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 264621,
|
||||||
|
"span_AD": 2.09,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 113.01,
|
||||||
|
"std": 0.33
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 85.41,
|
||||||
|
"std": 0.78
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 84.86,
|
||||||
|
"std": 0.93
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 84.19,
|
||||||
|
"std": 0.8
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 83.25,
|
||||||
|
"std": 1.19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 68534,
|
||||||
|
"span_AD": 7.58,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 113.01,
|
||||||
|
"std": 0.32
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 90.43,
|
||||||
|
"std": 1.1
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 88.21,
|
||||||
|
"std": 0.55
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 86.72,
|
||||||
|
"std": 0.44
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 82.85,
|
||||||
|
"std": 0.08
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-8111_model.json
Normal file
34
scripts/analysis/C-8111_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-8111",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.07625525560006084,
|
||||||
|
0.039701856173688654,
|
||||||
|
13.98648334581965
|
||||||
|
],
|
||||||
|
"intercept": -960.7734208999705,
|
||||||
|
"linear_r2": 0.659,
|
||||||
|
"gbm_r2": 0.8313,
|
||||||
|
"valve_poly": [
|
||||||
|
1.8890153721106516e-06,
|
||||||
|
-0.0015596046011240288,
|
||||||
|
0.5296293071190988,
|
||||||
|
-14.478452900869977
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 650.8,
|
||||||
|
"product": 581.9,
|
||||||
|
"T_C": 83.3
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 781.4,
|
||||||
|
"product": 707.5,
|
||||||
|
"T_C": 86.5
|
||||||
|
},
|
||||||
|
"n_operating_points": 280,
|
||||||
|
"n_prod_rows": 199945
|
||||||
|
}
|
||||||
67
scripts/analysis/C-8111_tempref.json
Normal file
67
scripts/analysis/C-8111_tempref.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"column": "C-8111",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 2,
|
||||||
|
"period": "2026-03-27~2026-06-05",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 28459,
|
||||||
|
"span_AD": 15.16,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 49.97,
|
||||||
|
"std": 0.14
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 93.38,
|
||||||
|
"std": 1.02
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 91.57,
|
||||||
|
"std": 0.91
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 83.97,
|
||||||
|
"std": 0.54
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 78.22,
|
||||||
|
"std": 0.13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 171490,
|
||||||
|
"span_AD": 16.36,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 49.97,
|
||||||
|
"std": 0.18
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 94.92,
|
||||||
|
"std": 0.66
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 93.05,
|
||||||
|
"std": 0.62
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 85.18,
|
||||||
|
"std": 0.51
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 78.56,
|
||||||
|
"std": 0.12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-9111_model.json
Normal file
34
scripts/analysis/C-9111_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-9111",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.07154616245940783,
|
||||||
|
0.41909641263227626,
|
||||||
|
-23.77111363563102
|
||||||
|
],
|
||||||
|
"intercept": 2169.7017492750356,
|
||||||
|
"linear_r2": 0.746,
|
||||||
|
"gbm_r2": 0.9968,
|
||||||
|
"valve_poly": [
|
||||||
|
5.592609787272492e-09,
|
||||||
|
-8.2019564706617e-06,
|
||||||
|
0.03620454531728825,
|
||||||
|
17.7448717037642
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 254.0,
|
||||||
|
"product": 176.7,
|
||||||
|
"T_C": 73.5
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 1427.6,
|
||||||
|
"product": 1342.0,
|
||||||
|
"T_C": 82.5
|
||||||
|
},
|
||||||
|
"n_operating_points": 161,
|
||||||
|
"n_prod_rows": 107198
|
||||||
|
}
|
||||||
40
scripts/analysis/C-9111_tempref.json
Normal file
40
scripts/analysis/C-9111_tempref.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"column": "C-9111",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 1,
|
||||||
|
"period": "2026-02-26~2026-06-05",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 163236,
|
||||||
|
"span_AD": 2.29,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 76.99,
|
||||||
|
"std": 6.51
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 83.63,
|
||||||
|
"std": 1.82
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 83.49,
|
||||||
|
"std": 2.31
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 82.16,
|
||||||
|
"std": 3.69
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 81.54,
|
||||||
|
"std": 4.32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
34
scripts/analysis/C-9211_model.json
Normal file
34
scripts/analysis/C-9211_model.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"column": "C-9211",
|
||||||
|
"features": [
|
||||||
|
"feed",
|
||||||
|
"product",
|
||||||
|
"T_C"
|
||||||
|
],
|
||||||
|
"linear_coeffs": [
|
||||||
|
0.22082559721551914,
|
||||||
|
0.41742853431313043,
|
||||||
|
6.570197052019047
|
||||||
|
],
|
||||||
|
"intercept": -479.59199642741356,
|
||||||
|
"linear_r2": 0.5715,
|
||||||
|
"gbm_r2": 0.9835,
|
||||||
|
"valve_poly": [
|
||||||
|
5.864565272695953e-07,
|
||||||
|
-0.0006161781631431878,
|
||||||
|
0.28501938580083425,
|
||||||
|
-15.01586412685447
|
||||||
|
],
|
||||||
|
"envelope_lo": {
|
||||||
|
"feed": 269.7,
|
||||||
|
"product": 98.7,
|
||||||
|
"T_C": 74.7
|
||||||
|
},
|
||||||
|
"envelope_hi": {
|
||||||
|
"feed": 453.6,
|
||||||
|
"product": 397.4,
|
||||||
|
"T_C": 90.2
|
||||||
|
},
|
||||||
|
"n_operating_points": 242,
|
||||||
|
"n_prod_rows": 165829
|
||||||
|
}
|
||||||
94
scripts/analysis/C-9211_tempref.json
Normal file
94
scripts/analysis/C-9211_tempref.json
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"column": "C-9211",
|
||||||
|
"stages_order": [
|
||||||
|
"reb_temp",
|
||||||
|
"T_B",
|
||||||
|
"T_C",
|
||||||
|
"T_D"
|
||||||
|
],
|
||||||
|
"n_products": 3,
|
||||||
|
"period": "2026-02-26~2026-06-04",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"label": "P0",
|
||||||
|
"n_rows": 8412,
|
||||||
|
"span_AD": 10.75,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 40.13,
|
||||||
|
"std": 3.79
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 85.1,
|
||||||
|
"std": 2.34
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 83.29,
|
||||||
|
"std": 2.14
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 77.03,
|
||||||
|
"std": 1.44
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 74.15,
|
||||||
|
"std": 1.57
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P1",
|
||||||
|
"n_rows": 138164,
|
||||||
|
"span_AD": 9.43,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 50.01,
|
||||||
|
"std": 3.82
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 88.31,
|
||||||
|
"std": 0.79
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 87.03,
|
||||||
|
"std": 0.72
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 81.37,
|
||||||
|
"std": 0.97
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 78.88,
|
||||||
|
"std": 1.79
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "P2",
|
||||||
|
"n_rows": 19405,
|
||||||
|
"span_AD": 12.77,
|
||||||
|
"vacuum": {
|
||||||
|
"median": 79.57,
|
||||||
|
"std": 14.99
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"reb_temp": {
|
||||||
|
"median": 93.72,
|
||||||
|
"std": 1.99
|
||||||
|
},
|
||||||
|
"T_B": {
|
||||||
|
"median": 92.58,
|
||||||
|
"std": 2.04
|
||||||
|
},
|
||||||
|
"T_C": {
|
||||||
|
"median": 89.38,
|
||||||
|
"std": 3.57
|
||||||
|
},
|
||||||
|
"T_D": {
|
||||||
|
"median": 79.21,
|
||||||
|
"std": 5.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ FEATURES = ["feed", "product", "T_C"]
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
parser.add_argument("--output", help="JSON 출력 경로 (기본: scripts/analysis/{prefix}_model.json)")
|
parser.add_argument("--output", help="JSON 출력 경로 (기본: scripts/analysis/{prefix}_model.json)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data)
|
df = pd.read_pickle(args.data)
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ def main():
|
|||||||
for m, n in vc.items():
|
for m, n in vc.items():
|
||||||
print(f" {m:9s} {n:7d} {100*n/len(df):5.1f}% ≈ {n*30/3600:7.1f} h")
|
print(f" {m:9s} {n:7d} {100*n/len(df):5.1f}% ≈ {n*30/3600:7.1f} h")
|
||||||
|
|
||||||
out = "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_data.pkl"
|
out = "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_data.pkl"
|
||||||
df.to_pickle(out)
|
df.to_pickle(out)
|
||||||
plot_timeline(df, "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_timeline.png")
|
plot_timeline(df, "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_timeline.png")
|
||||||
print(f"저장: {out}")
|
print(f"저장: {out}")
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ class OperatorAssist:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
parser.add_argument("--live", help='JSON live_tags for single predict test')
|
parser.add_argument("--live", help='JSON live_tags for single predict test')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data)
|
df = pd.read_pickle(args.data)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ OP_RESAMPLE = "6h"
|
|||||||
|
|
||||||
def load(data_path=None):
|
def load(data_path=None):
|
||||||
if data_path is None:
|
if data_path is None:
|
||||||
data_path = BASE + "c6111_data.pkl"
|
data_path = BASE + "C-6111_data.pkl"
|
||||||
df = pd.read_pickle(data_path)
|
df = pd.read_pickle(data_path)
|
||||||
df = df[df["mode"] == "PROD"].copy()
|
df = df[df["mode"] == "PROD"].copy()
|
||||||
# 엔지니어링 피처: 온도 구배(분리도)
|
# 엔지니어링 피처: 온도 구배(분리도)
|
||||||
@@ -100,7 +100,7 @@ def regress(df):
|
|||||||
return ops, gbm, Xte, yte, gbm.predict(Xte), imp
|
return ops, gbm, Xte, yte, gbm.predict(Xte), imp
|
||||||
|
|
||||||
|
|
||||||
def plots(hb, ops, yte, pred, imp, prefix="c6111"):
|
def plots(hb, ops, yte, pred, imp, prefix="C-6111"):
|
||||||
fig, ax = plt.subplots(1, 4, figsize=(22, 5))
|
fig, ax = plt.subplots(1, 4, figsize=(22, 5))
|
||||||
ax[0].scatter(hb["op"], hb["flow"], s=20, c="k", label="mean")
|
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_up"], "b.-", ms=4, label="OP rising")
|
||||||
@@ -121,8 +121,8 @@ def plots(hb, ops, yte, pred, imp, prefix="c6111"):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = load(args.data)
|
df = load(args.data)
|
||||||
print(f"PROD 정합데이터 {len(df)}행")
|
print(f"PROD 정합데이터 {len(df)}행")
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ RETRAIN_EVERY = "1D"
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data)
|
df = pd.read_pickle(args.data)
|
||||||
df = df[df["mode"] == "PROD"].copy()
|
df = df[df["mode"] == "PROD"].copy()
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ class SteamPredictor:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data)
|
df = pd.read_pickle(args.data)
|
||||||
df = df[df["mode"] == "PROD"].copy()
|
df = df[df["mode"] == "PROD"].copy()
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ def shutdown_milestones(df, co):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data).sort_values("dtat").reset_index(drop=True)
|
df = pd.read_pickle(args.data).sort_values("dtat").reset_index(drop=True)
|
||||||
cutoffs = detect_cutoffs(df)
|
cutoffs = detect_cutoffs(df)
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ def milestones(df, ci):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--data", default=BASE + "c6111_data.pkl")
|
parser.add_argument("--data", default=BASE + "C-6111_data.pkl")
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
df = pd.read_pickle(args.data).sort_values("dtat").reset_index(drop=True)
|
df = pd.read_pickle(args.data).sort_values("dtat").reset_index(drop=True)
|
||||||
cutins = detect_cutins(df)
|
cutins = detect_cutins(df)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ MOVE = 0.1 # OP 변경 인식 임계(%)
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
df = pd.read_pickle(BASE + "c6111_data.pkl")
|
df = pd.read_pickle(BASE + "C-6111_data.pkl")
|
||||||
df = df[df["mode"] == "PROD"].copy().sort_values("dtat").reset_index(drop=True)
|
df = df[df["mode"] == "PROD"].copy().sort_values("dtat").reset_index(drop=True)
|
||||||
df = df[(df["feed"] > 50) & (df["steam_op"] > 1)]
|
df = df[(df["feed"] > 50) & (df["steam_op"] > 1)]
|
||||||
|
|
||||||
|
|||||||
@@ -359,8 +359,8 @@ def _nanmid(s):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Export plot data as JSON for web dashboard")
|
parser = argparse.ArgumentParser(description="Export plot data as JSON for web dashboard")
|
||||||
parser.add_argument("--data", default=os.path.join(BASE, "c6111_data.pkl"))
|
parser.add_argument("--data", default=os.path.join(BASE, "C-6111_data.pkl"))
|
||||||
parser.add_argument("--prefix", default="c6111")
|
parser.add_argument("--prefix", default="C-6111")
|
||||||
parser.add_argument("--output", default=None, help="Output path (default: data/{prefix}_plotdata.json)")
|
parser.add_argument("--output", default=None, help="Output path (default: data/{prefix}_plotdata.json)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ def cluster_products(reb):
|
|||||||
|
|
||||||
|
|
||||||
def build(prefix, stable_from=None, stable_to=None):
|
def build(prefix, stable_from=None, stable_to=None):
|
||||||
pkl = os.path.join(BASE, f"c{prefix}_data.pkl")
|
pkl = os.path.join(BASE, f"{prefix}_data.pkl")
|
||||||
if prefix == "61" and not os.path.exists(pkl):
|
if prefix == "C-6111" and not os.path.exists(pkl):
|
||||||
pkl = os.path.join(BASE, "c6111_data.pkl")
|
pkl = os.path.join(BASE, "c6111_data.pkl")
|
||||||
if not os.path.exists(pkl):
|
if not os.path.exists(pkl):
|
||||||
print(f" [skip] {prefix}: {pkl} 없음")
|
print(f" [skip] {prefix}: {pkl} 없음")
|
||||||
@@ -68,14 +68,14 @@ def build(prefix, stable_from=None, stable_to=None):
|
|||||||
"std": round(float(g["vacuum"].std()), 2)},
|
"std": round(float(g["vacuum"].std()), 2)},
|
||||||
"stages": stages,
|
"stages": stages,
|
||||||
})
|
})
|
||||||
ref = {"column": f"c{prefix}", "stages_order": STAGES,
|
ref = {"column": prefix, "stages_order": STAGES,
|
||||||
"n_products": len(products),
|
"n_products": len(products),
|
||||||
"period": f"{df['dtat'].min():%Y-%m-%d}~{df['dtat'].max():%Y-%m-%d}",
|
"period": f"{df['dtat'].min():%Y-%m-%d}~{df['dtat'].max():%Y-%m-%d}",
|
||||||
"products": products}
|
"products": products}
|
||||||
out = os.path.join(BASE, f"c{prefix}_tempref.json")
|
out = os.path.join(BASE, f"{prefix}_tempref.json")
|
||||||
with open(out, "w") as f:
|
with open(out, "w") as f:
|
||||||
json.dump(ref, f, indent=2, ensure_ascii=False)
|
json.dump(ref, f, indent=2, ensure_ascii=False)
|
||||||
print(f" c{prefix}: 제품 {len(products)}개 ", end="")
|
print(f" {prefix}: 제품 {len(products)}개 ", end="")
|
||||||
for p in products:
|
for p in products:
|
||||||
s = p["stages"]
|
s = p["stages"]
|
||||||
print(f"[{p['label']} reb{s['reb_temp']['median']:.1f}/Tc{s['T_C']['median']:.1f}/"
|
print(f"[{p['label']} reb{s['reb_temp']['median']:.1f}/Tc{s['T_C']['median']:.1f}/"
|
||||||
@@ -90,7 +90,7 @@ def main():
|
|||||||
ap.add_argument("--from", dest="stable_from", help="안정구간 시작 YYYY-MM-DD")
|
ap.add_argument("--from", dest="stable_from", help="안정구간 시작 YYYY-MM-DD")
|
||||||
ap.add_argument("--to", dest="stable_to", help="안정구간 끝")
|
ap.add_argument("--to", dest="stable_to", help="안정구간 끝")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
prefixes = [args.prefix] if args.prefix else ["61", "62", "81", "91", "92", "101", "102"]
|
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:
|
for p in prefixes:
|
||||||
build(p, args.stable_from, args.stable_to)
|
build(p, args.stable_from, args.stable_to)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ PY = sys.executable
|
|||||||
|
|
||||||
|
|
||||||
def extract(prefix, asset):
|
def extract(prefix, asset):
|
||||||
"""추출 + 운전모드 분류. c{prefix}_data.pkl 저장."""
|
"""추출 + 운전모드 분류. C-{prefix}11_data.pkl 저장."""
|
||||||
from c6111_extract import roles_for, tag_frame, classify_phases, clip_to_ranges
|
from c6111_extract import roles_for, tag_frame, classify_phases, clip_to_ranges
|
||||||
|
|
||||||
with psycopg.connect(DSN) as conn:
|
with psycopg.connect(DSN) as conn:
|
||||||
@@ -48,7 +48,8 @@ def extract(prefix, asset):
|
|||||||
|
|
||||||
df = clip_to_ranges(df, roles) # 계기 EU range 밖 스파이크 → NaN
|
df = clip_to_ranges(df, roles) # 계기 EU range 밖 스파이크 → NaN
|
||||||
df["mode"] = classify_phases(df)
|
df["mode"] = classify_phases(df)
|
||||||
out = os.path.join(BASE, f"c{prefix}_data.pkl")
|
col_key = f"C-{prefix}11"
|
||||||
|
out = os.path.join(BASE, f"{col_key}_data.pkl")
|
||||||
df.to_pickle(out)
|
df.to_pickle(out)
|
||||||
|
|
||||||
print(f"\n=== {prefix} ({asset}) ===")
|
print(f"\n=== {prefix} ({asset}) ===")
|
||||||
@@ -62,8 +63,9 @@ def extract(prefix, asset):
|
|||||||
|
|
||||||
def run_analysis(script, prefix):
|
def run_analysis(script, prefix):
|
||||||
"""분석 스크립트 1개 실행 (subprocess)."""
|
"""분석 스크립트 1개 실행 (subprocess)."""
|
||||||
data = os.path.join(BASE, f"c{prefix}_data.pkl")
|
col_key = f"C-{prefix}11"
|
||||||
cmd = [PY, os.path.join(BASE, script), "--data", data, "--prefix", f"c{prefix}"]
|
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)}")
|
print(f"\n>>> {' '.join(cmd)}")
|
||||||
r = subprocess.run(cmd)
|
r = subprocess.run(cmd)
|
||||||
return r.returncode
|
return r.returncode
|
||||||
@@ -87,11 +89,12 @@ def compare():
|
|||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for prefix, asset, label in COLUMNS:
|
for prefix, asset, label in COLUMNS:
|
||||||
pkl = os.path.join(BASE, f"c{prefix}_data.pkl")
|
col_key = f"C-{prefix}11"
|
||||||
# 6-1 legacy: c6111_data.pkl (not c61_data.pkl)
|
pkl = os.path.join(BASE, f"{col_key}_data.pkl")
|
||||||
if prefix == "61" and not os.path.exists(pkl):
|
# 6-1 legacy: c6111_data.pkl
|
||||||
|
if not os.path.exists(pkl):
|
||||||
alt = os.path.join(BASE, "c6111_data.pkl")
|
alt = os.path.join(BASE, "c6111_data.pkl")
|
||||||
if os.path.exists(alt):
|
if prefix == "61" and os.path.exists(alt):
|
||||||
pkl = alt
|
pkl = alt
|
||||||
if not os.path.exists(pkl):
|
if not os.path.exists(pkl):
|
||||||
print(f" [skip] {label}: {pkl} 없음")
|
print(f" [skip] {label}: {pkl} 없음")
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ public sealed class FeedforwardController : ControllerBase
|
|||||||
tcReturnRebBand = c.TcReturnRebBand,
|
tcReturnRebBand = c.TcReturnRebBand,
|
||||||
tcReturnDeltaAdRef = double.IsNaN(c.TcReturnDeltaAdRef) ? (double?)null : c.TcReturnDeltaAdRef,
|
tcReturnDeltaAdRef = double.IsNaN(c.TcReturnDeltaAdRef) ? (double?)null : c.TcReturnDeltaAdRef,
|
||||||
tcReturnDeltaAdBand = c.TcReturnDeltaAdBand,
|
tcReturnDeltaAdBand = c.TcReturnDeltaAdBand,
|
||||||
|
tcReturnTcTarget = double.IsNaN(c.TcReturnTcTarget) ? (double?)null : c.TcReturnTcTarget,
|
||||||
|
tcReturnTcBand = c.TcReturnTcBand,
|
||||||
streams = c.Streams.Select(s => new
|
streams = c.Streams.Select(s => new
|
||||||
{
|
{
|
||||||
key = s.Key, flowTag = s.FlowTag, role = s.Role.ToString(), levelTag = s.LevelTag, targetCoeff = s.TargetCoeff,
|
key = s.Key, flowTag = s.FlowTag, role = s.Role.ToString(), levelTag = s.LevelTag, targetCoeff = s.TargetCoeff,
|
||||||
@@ -264,8 +266,6 @@ public sealed class FeedforwardController : ControllerBase
|
|||||||
var adv = await _ramp.ComputeAsync(columnId, body.targetFeed, 50, double.NaN, double.NaN, double.NaN, 1.8, ct);
|
var adv = await _ramp.ComputeAsync(columnId, body.targetFeed, 50, double.NaN, double.NaN, double.NaN, 1.8, ct);
|
||||||
if (adv is null) return NotFound(new { error = "config 없음" });
|
if (adv is null) return NotFound(new { error = "config 없음" });
|
||||||
if (adv.Hold) return BadRequest(new { error = $"피드 불량 — 시작 불가: {string.Join(", ", adv.Warnings)}" });
|
if (adv.Hold) return BadRequest(new { error = $"피드 불량 — 시작 불가: {string.Join(", ", adv.Warnings)}" });
|
||||||
if (body.targetFeed <= adv.CurrentFeed)
|
|
||||||
return BadRequest(new { error = $"업램프만 지원 — 목표({body.targetFeed:F1})가 현재 피드({adv.CurrentFeed:F1}) 이하" });
|
|
||||||
|
|
||||||
bool dryRun = RampDryRun() || _sim.Enabled;
|
bool dryRun = RampDryRun() || _sim.Enabled;
|
||||||
var job = _rampJobs.Start(columnId, body.targetFeed, "manual", dryRun);
|
var job = _rampJobs.Start(columnId, body.targetFeed, "manual", dryRun);
|
||||||
|
|||||||
@@ -278,7 +278,8 @@ public class Hc900TagManagerController : ControllerBase
|
|||||||
var tagNames = entries.Select(e => e.TagName).ToList();
|
var tagNames = entries.Select(e => e.TagName).ToList();
|
||||||
var liveValues = await _ctx.RealtimePoints
|
var liveValues = await _ctx.RealtimePoints
|
||||||
.Where(r => tagNames.Contains(r.TagName))
|
.Where(r => tagNames.Contains(r.TagName))
|
||||||
.ToDictionaryAsync(r => r.TagName, r => new { r.LiveValue, r.Timestamp });
|
.GroupBy(r => r.TagName)
|
||||||
|
.ToDictionaryAsync(g => g.Key, g => { var first = g.OrderByDescending(r => r.Timestamp).FirstOrDefault(); return new { first?.LiveValue, Timestamp = first?.Timestamp }; });
|
||||||
|
|
||||||
var result = entries.Select(e =>
|
var result = entries.Select(e =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ public sealed class SteamAdvisorController : ControllerBase
|
|||||||
.Select(c => c.Key)
|
.Select(c => c.Key)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
return Ok(new { columns, configured = configured.OrderBy(x => x).ToList() });
|
var defaultCol = _config.GetValue<string>("SteamAdvisor:DefaultColumn") ?? "C-6111";
|
||||||
|
return Ok(new { columns, configured = configured.OrderBy(x => x).ToList(), defaultColumn = defaultCol });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("backtest/{col}")]
|
[HttpGet("backtest/{col}")]
|
||||||
@@ -115,7 +116,7 @@ public sealed class SteamAdvisorController : ControllerBase
|
|||||||
[HttpGet("live")]
|
[HttpGet("live")]
|
||||||
public async Task<IActionResult> Live([FromQuery] string? col = null)
|
public async Task<IActionResult> Live([FromQuery] string? col = null)
|
||||||
{
|
{
|
||||||
col ??= _config.GetValue<string>("SteamAdvisor:DefaultColumn") ?? "c6111";
|
col ??= _config.GetValue<string>("SteamAdvisor:DefaultColumn") ?? "C-6111";
|
||||||
var tags = _config.GetSection($"SteamAdvisor:Columns:{col}");
|
var tags = _config.GetSection($"SteamAdvisor:Columns:{col}");
|
||||||
var feedTag = tags["Feed"];
|
var feedTag = tags["Feed"];
|
||||||
var productTag = tags["Product"];
|
var productTag = tags["Product"];
|
||||||
@@ -132,7 +133,8 @@ public sealed class SteamAdvisorController : ControllerBase
|
|||||||
|
|
||||||
var live = await _ctx.RealtimePoints
|
var live = await _ctx.RealtimePoints
|
||||||
.Where(r => tagNames.Contains(r.TagName))
|
.Where(r => tagNames.Contains(r.TagName))
|
||||||
.ToDictionaryAsync(r => r.TagName, r => r.LiveValue);
|
.GroupBy(r => r.TagName)
|
||||||
|
.ToDictionaryAsync(g => g.Key, g => g.OrderByDescending(r => r.Timestamp).FirstOrDefault()?.LiveValue);
|
||||||
|
|
||||||
if (!live.ContainsKey(feedTag) || !live.ContainsKey(productTag) || !live.ContainsKey(tcTag))
|
if (!live.ContainsKey(feedTag) || !live.ContainsKey(productTag) || !live.ContainsKey(tcTag))
|
||||||
{
|
{
|
||||||
@@ -173,27 +175,28 @@ public sealed class SteamAdvisorController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 레이어②: 컬럼 온도 프로파일 이격 모니터 ────────────────────────
|
// ── 레이어②: 컬럼 온도 프로파일 이격 모니터 ────────────────────────
|
||||||
// realtime 단별 온도/진공 vs 기준밴드(c{col}_tempref.json) → 제품매칭 + z-score 이격.
|
// realtime 단별 온도/진공 vs 기준밴드({col}_tempref.json) → 제품매칭 + z-score 이격.
|
||||||
// col 규약 = 분석 prefix("61","62","81","91","92","101","102").
|
// col 규약 = 컬럼키("C-6111","C-6211","C-8111",...).
|
||||||
[HttpGet("tempprofile/{col}")]
|
[HttpGet("tempprofile/{col}")]
|
||||||
public async Task<IActionResult> TempProfile(string col)
|
public async Task<IActionResult> TempProfile(string col)
|
||||||
{
|
{
|
||||||
var dir = _config.GetValue<string>("SteamAdvisor:ModelDir")
|
var dir = _config.GetValue<string>("SteamAdvisor:ModelDir")
|
||||||
?? "/home/windpacer/projects/hc900_ax/scripts/analysis";
|
?? "/home/windpacer/projects/hc900_ax/scripts/analysis";
|
||||||
var path = Path.Combine(dir, $"c{col}_tempref.json");
|
var path = Path.Combine(dir, $"{col}_tempref.json");
|
||||||
if (!System.IO.File.Exists(path))
|
if (!System.IO.File.Exists(path))
|
||||||
return NotFound(new { error = $"기준 프로파일 없음: c{col}_tempref.json" });
|
return NotFound(new { error = $"기준 프로파일 없음: {col}_tempref.json" });
|
||||||
|
|
||||||
var tref = JsonSerializer.Deserialize<TempRef>(
|
var tref = JsonSerializer.Deserialize<TempRef>(
|
||||||
await System.IO.File.ReadAllTextAsync(path),
|
await System.IO.File.ReadAllTextAsync(path),
|
||||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
if (tref is null) return StatusCode(500, new { error = "tempref 파싱 실패" });
|
if (tref is null) return StatusCode(500, new { error = "tempref 파싱 실패" });
|
||||||
|
|
||||||
var tagMap = TagsFor(col);
|
var tagMap = TagsFor(ToSuffix(col));
|
||||||
var tagNames = tagMap.Values.ToArray();
|
var tagNames = tagMap.Values.ToArray();
|
||||||
var live = await _ctx.RealtimePoints
|
var live = await _ctx.RealtimePoints
|
||||||
.Where(r => tagNames.Contains(r.TagName))
|
.Where(r => tagNames.Contains(r.TagName))
|
||||||
.ToDictionaryAsync(r => r.TagName, r => r.LiveValue);
|
.GroupBy(r => r.TagName)
|
||||||
|
.ToDictionaryAsync(g => g.Key, g => g.OrderByDescending(r => r.Timestamp).FirstOrDefault()?.LiveValue);
|
||||||
double? Val(string tag)
|
double? Val(string tag)
|
||||||
=> live.TryGetValue(tag, out var s) && double.TryParse(s, out var v) ? v : null;
|
=> live.TryGetValue(tag, out var s) && double.TryParse(s, out var v) ? v : null;
|
||||||
var cur = tagMap.ToDictionary(kv => kv.Key, kv => Val(kv.Value));
|
var cur = tagMap.ToDictionary(kv => kv.Key, kv => Val(kv.Value));
|
||||||
@@ -244,27 +247,30 @@ public sealed class SteamAdvisorController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// roles_for(C# 미러) — 단별 온도/진공 태그. c6111_extract COLUMN_EXCEPTIONS 대응.
|
// roles_for(C# 미러) — 단별 온도/진공 태그. c6111_extract COLUMN_EXCEPTIONS 대응.
|
||||||
|
// p = numeric suffix ("6111", "6211", "8111", ...)
|
||||||
private static Dictionary<string, string> TagsFor(string p)
|
private static Dictionary<string, string> TagsFor(string p)
|
||||||
{
|
{
|
||||||
var m = new Dictionary<string, string>
|
var m = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["reb_temp"] = $"TICA-{p}11A.PV",
|
["reb_temp"] = $"TICA-{p}A.PV",
|
||||||
["T_B"] = $"TI-{p}11B.PV",
|
["T_B"] = $"TI-{p}B.PV",
|
||||||
["T_C"] = $"TI-{p}11C.PV",
|
["T_C"] = $"TI-{p}C.PV",
|
||||||
["T_D"] = $"TI-{p}11D.PV",
|
["T_D"] = $"TI-{p}D.PV",
|
||||||
["vacuum"] = $"PICA-{p}11.PV",
|
["vacuum"] = $"PICA-{p}.PV",
|
||||||
};
|
};
|
||||||
switch (p)
|
switch (p)
|
||||||
{
|
{
|
||||||
case "51": m["T_C"] = "TI-5111B.PV"; break;
|
case "8111": m["reb_temp"] = "TICA-8111.PV"; m["vacuum"] = "PICA-8111A.PV"; break;
|
||||||
case "81": m["reb_temp"] = "TICA-8111.PV"; m["vacuum"] = "PICA-8111A.PV"; break;
|
case "9111": m["vacuum"] = "PICA-9111A.PV"; break;
|
||||||
case "91": m["vacuum"] = "PICA-9111A.PV"; break;
|
case "9211": m["vacuum"] = "PICA-9211A.PV"; break;
|
||||||
case "92": m["vacuum"] = "PICA-9211A.PV"; break;
|
case "10111": m["vacuum"] = "PICA-10111A.PV"; break;
|
||||||
case "101": m["vacuum"] = "PICA-10111A.PV"; break;
|
case "10211": m["vacuum"] = "PICA-10211A.PV"; break;
|
||||||
case "102": m["vacuum"] = "PICA-10211A.PV"; break;
|
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 컬럼키 "C-6111" → numeric suffix "6111"
|
||||||
|
private static string ToSuffix(string col) => col.StartsWith("C-") ? col[2..] : col;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── tempref.json 역직렬화 (gen_temp_profiles.py 산출 구조) ──
|
// ── tempref.json 역직렬화 (gen_temp_profiles.py 산출 구조) ──
|
||||||
|
|||||||
@@ -65,19 +65,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SteamAdvisor": {
|
"SteamAdvisor": {
|
||||||
"ModelPath": "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_model.json",
|
"ModelPath": "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_model.json",
|
||||||
"PlotDataDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
"PlotDataDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
||||||
"ModelDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
"ModelDir": "/home/windpacer/projects/hc900_ax/scripts/analysis",
|
||||||
"DefaultColumn": "c6111",
|
"DefaultColumn": "C-6111",
|
||||||
"Columns": {
|
"Columns": {
|
||||||
"c6111": { "Feed": "FICQ-6101.PV", "Product": "FICQ-6118.PV", "TC": "TI-6111C", "SteamOp": "TICA-6111A.OP", "SteamFlow": "FIQ-6115" },
|
"C-6111": { "Feed": "FICQ-6101.PV", "Product": "FICQ-6118.PV", "TC": "TI-6111C", "SteamOp": "TICA-6111A.OP", "SteamFlow": "FIQ-6115" },
|
||||||
"c61": { "Feed": "FICQ-6101.PV", "Product": "FICQ-6118.PV", "TC": "TI-6111C", "SteamOp": "TICA-6111A.OP", "SteamFlow": "FIQ-6115" },
|
"C-6211": { "Feed": "FICQ-6201.PV", "Product": "FICQ-6218.PV", "TC": "TI-6211C", "SteamOp": "TICA-6211A.OP", "SteamFlow": "FIQ-6215" },
|
||||||
"c62": { "Feed": "FICQ-6201.PV", "Product": "FICQ-6218.PV", "TC": "TI-6211C", "SteamOp": "TICA-6211A.OP", "SteamFlow": "FIQ-6215" },
|
"C-8111": { "Feed": "FICQ-8101.PV", "Product": "FICQ-8118.PV", "TC": "TI-8111C", "SteamOp": "TICA-8111A.OP", "SteamFlow": "FIQ-8115" },
|
||||||
"c81": { "Feed": "FIQ-8111.PV", "Product": "FICQ-8118.PV", "TC": "TI-8111C", "SteamOp": "TICA-8111A.OP", "SteamFlow": "FIQ-8115" },
|
"C-9111": { "Feed": "FICQ-9101.PV", "Product": "FICQ-9118.PV", "TC": "TI-9111C", "SteamOp": "TICA-9111A.OP", "SteamFlow": "FIQ-9115" },
|
||||||
"c91": { "Feed": "FIQ-9111.PV", "Product": "FICQ-9118.PV", "TC": "TI-9111C", "SteamOp": "TICA-9111A.OP", "SteamFlow": "FIQ-9115" },
|
"C-9211": { "Feed": "FICQ-9201.PV", "Product": "FICQ-9218.PV", "TC": "TI-9211C", "SteamOp": "TICA-9211A.OP", "SteamFlow": "FIQ-9215" },
|
||||||
"c92": { "Feed": "FIQ-9211.PV", "Product": "FICQ-9218.PV", "TC": "TI-9211C", "SteamOp": "TICA-9211A.OP", "SteamFlow": "FIQ-9215" },
|
"C-10111": { "Feed": "FICQ-10101.PV", "Product": "FICQ-10118.PV", "TC": "TI-10111C", "SteamOp": "TICA-10111A.OP", "SteamFlow": "FIQ-10115" },
|
||||||
"c101": { "Feed": "FIQ-10111.PV", "Product": "FICQ-10118.PV", "TC": "TI-10111C", "SteamOp": "TICA-10111A.OP", "SteamFlow": "FIQ-10115" },
|
"C-10211": { "Feed": "FICQ-10201.PV", "Product": "FICQ-10218.PV", "TC": "TI-10211C", "SteamOp": "TICA-10211A.OP", "SteamFlow": "FIQ-10215" }
|
||||||
"c102": { "Feed": "FIQ-10211.PV", "Product": "FICQ-10218.PV", "TC": "TI-10211C", "SteamOp": "TICA-10211A.OP", "SteamFlow": "FIQ-10215" }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
.ff-mode{font-size:12px;font-weight:600;padding:2px 8px;border-radius:10px}
|
.ff-mode{font-size:12px;font-weight:600;padding:2px 8px;border-radius:10px}
|
||||||
.ff-mode-rec{background:#5a3000;color:#ffb74d}
|
.ff-mode-rec{background:#5a3000;color:#ffb74d}
|
||||||
.ff-mode-ret{background:#003a4d;color:#7fd1ff}
|
.ff-mode-ret{background:#003a4d;color:#7fd1ff}
|
||||||
|
.ff-mode-nrm{background:#14532d;color:#69f0ae}
|
||||||
.ff-mode-arm{background:#5a0000;color:#ff8a80;animation:ffblink 1s step-start infinite}
|
.ff-mode-arm{background:#5a0000;color:#ff8a80;animation:ffblink 1s step-start infinite}
|
||||||
@keyframes ffblink{50%{opacity:.4}}
|
@keyframes ffblink{50%{opacity:.4}}
|
||||||
/* WO-7 설정폼 신규 섹션 */
|
/* WO-7 설정폼 신규 섹션 */
|
||||||
@@ -102,6 +103,9 @@
|
|||||||
.ff-ramp-warn{font-size:11px;color:#ffb74d;margin-top:6px;line-height:1.5}
|
.ff-ramp-warn{font-size:11px;color:#ffb74d;margin-top:6px;line-height:1.5}
|
||||||
.ff-ramp-hold{color:#ff8a80;font-weight:600}
|
.ff-ramp-hold{color:#ff8a80;font-weight:600}
|
||||||
.ff-ramp-err{color:#ff5252}
|
.ff-ramp-err{color:#ff5252}
|
||||||
|
.ff-ramp-dir{font-size:12px;font-weight:600;padding:1px 6px;border-radius:6px}
|
||||||
|
.ff-ramp-up{background:#14532d;color:#69f0ae}
|
||||||
|
.ff-ramp-dn{background:#1e3a5f;color:#bfdbfe}
|
||||||
/* ── WP7: 온도 프로파일 상태 뱃지 ────────────────────── */
|
/* ── WP7: 온도 프로파일 상태 뱃지 ────────────────────── */
|
||||||
.ff-tp-badge{font-size:12px;padding:2px 8px;border-radius:10px;display:inline-block;margin-top:4px}
|
.ff-tp-badge{font-size:12px;padding:2px 8px;border-radius:10px;display:inline-block;margin-top:4px}
|
||||||
.ff-tp-ok{background:#003a1a;color:#69f0ae}
|
.ff-tp-ok{background:#003a1a;color:#69f0ae}
|
||||||
|
|||||||
@@ -62,10 +62,16 @@ async function ffRampCompute() {
|
|||||||
host.innerHTML = `<div class="ff-ramp-hold">HOLD: ${esc(data.warnings?.join(', ') || '피드 불량')}</div>`;
|
host.innerHTML = `<div class="ff-ramp-hold">HOLD: ${esc(data.warnings?.join(', ') || '피드 불량')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const goingUp = data.clampedTarget != null && data.clampedTarget > data.currentFeed;
|
||||||
|
const goingDn = data.clampedTarget != null && data.clampedTarget < data.currentFeed;
|
||||||
|
const dirBadge = goingUp ? '<span class="ff-ramp-dir ff-ramp-up">↑ 상승</span>' : goingDn ? '<span class="ff-ramp-dir ff-ramp-dn">↓ 하강</span>' : '<span class="ff-ramp-dir">—</span>';
|
||||||
|
const curEl = document.getElementById('ff-ramp-currentFeed');
|
||||||
|
if (curEl) curEl.value = data.currentFeed;
|
||||||
host.innerHTML = `
|
host.innerHTML = `
|
||||||
<table class="ff-ramp-table">
|
<table class="ff-ramp-table">
|
||||||
<tr><td>현재 피드</td><td class="ff-num">${fmtVal(data.currentFeed)}</td></tr>
|
<tr><td>현재 피드</td><td class="ff-num">${fmtVal(data.currentFeed)}</td></tr>
|
||||||
<tr><td>목표 피드</td><td class="ff-num">${fmtVal(data.targetFeed)}</td></tr>
|
<tr><td>목표 피드</td><td class="ff-num">${fmtVal(data.targetFeed)}</td></tr>
|
||||||
|
<tr><td>방향</td><td>${dirBadge}</td></tr>
|
||||||
<tr><td>클램프 목표</td><td class="ff-num ff-rec">${fmtVal(data.clampedTarget)}</td></tr>
|
<tr><td>클램프 목표</td><td class="ff-num ff-rec">${fmtVal(data.clampedTarget)}</td></tr>
|
||||||
<tr><td>Ceiling</td><td class="ff-num">${fmtVal(data.ceiling?.value)} <small>(${esc(data.ceiling?.binding)})</small></td></tr>
|
<tr><td>Ceiling</td><td class="ff-num">${fmtVal(data.ceiling?.value)} <small>(${esc(data.ceiling?.binding)})</small></td></tr>
|
||||||
<tr><td>램프율</td><td class="ff-num">${fmtVal(data.rampRate?.value)} kg/hr·min <small>(${esc(data.rampRate?.binding)})</small></td></tr>
|
<tr><td>램프율</td><td class="ff-num">${fmtVal(data.rampRate?.value)} kg/hr·min <small>(${esc(data.rampRate?.binding)})</small></td></tr>
|
||||||
@@ -86,7 +92,8 @@ async function ffRampStart() {
|
|||||||
const target = +g('ff-ramp-targetFeed').value;
|
const target = +g('ff-ramp-targetFeed').value;
|
||||||
if (!Number.isFinite(colId) || !Number.isFinite(target)) { alert('columnId·targetFeed 확인'); return; }
|
if (!Number.isFinite(colId) || !Number.isFinite(target)) { alert('columnId·targetFeed 확인'); return; }
|
||||||
const mode = ffRampDryRun ? '모의(DryRun — 실제 쓰기 없음)' : '실쓰기';
|
const mode = ffRampDryRun ? '모의(DryRun — 실제 쓰기 없음)' : '실쓰기';
|
||||||
if (!confirm(`컬럼 ${colId} FEED를 ${target}까지 단계적으로 올립니다 [${mode}]. 시작하시겠습니까?`)) return;
|
const dir = target > +(g('ff-ramp-currentFeed')?.value || 0) ? '올립니다' : '내립니다';
|
||||||
|
if (!confirm(`컬럼 ${colId} FEED를 ${target}까지 단계적으로 ${dir} [${mode}]. 시작하시겠습니까?`)) return;
|
||||||
try {
|
try {
|
||||||
const r = await ffApi('POST', `/api/ff/feed-ramp/${colId}/start`, { targetFeed: target });
|
const r = await ffApi('POST', `/api/ff/feed-ramp/${colId}/start`, { targetFeed: target });
|
||||||
const modeEl = g('ff-ramp-mode');
|
const modeEl = g('ff-ramp-mode');
|
||||||
@@ -236,12 +243,13 @@ function ffRampCancel(id) {
|
|||||||
ffApi('POST', `/api/ff/feed-ramp/${id}/cancel`).then(() => ffLoadDash()).catch(e => alert(e.message));
|
ffApi('POST', `/api/ff/feed-ramp/${id}/cancel`).then(() => ffLoadDash()).catch(e => alert(e.message));
|
||||||
}
|
}
|
||||||
// 카드의 FEED Target SP로 램프 시작
|
// 카드의 FEED Target SP로 램프 시작
|
||||||
function ffCardRampStart(colId) {
|
function ffCardRampStart(colId, currentFeed) {
|
||||||
const inp = document.querySelector(`.ff-rt[data-col="${colId}"]`);
|
const inp = document.querySelector(`.ff-rt[data-col="${colId}"]`);
|
||||||
const target = inp ? +inp.value : NaN;
|
const target = inp ? +inp.value : NaN;
|
||||||
if (!Number.isFinite(target)) { alert('FEED Target SP를 입력하세요'); return; }
|
if (!Number.isFinite(target)) { alert('FEED Target SP를 입력하세요'); return; }
|
||||||
const mode = ffRampDryRun ? '모의(DryRun — 실제 쓰기 없음)' : '실쓰기';
|
const mode = ffRampDryRun ? '모의(DryRun — 실제 쓰기 없음)' : '실쓰기';
|
||||||
if (!confirm(`컬럼 ${colId} FEED를 ${target}까지 램프율로 점진 상승 [${mode}]. 시작?`)) return;
|
const dir = target > currentFeed ? '상승' : '하강';
|
||||||
|
if (!confirm(`컬럼 ${colId} FEED를 ${target}까지 램프율로 점진 ${dir} [${mode}]. 시작?`)) return;
|
||||||
ffApi('POST', `/api/ff/feed-ramp/${colId}/start`, { targetFeed: target })
|
ffApi('POST', `/api/ff/feed-ramp/${colId}/start`, { targetFeed: target })
|
||||||
.then(() => ffLoadDash()).catch(e => alert('램프 시작 실패: ' + e.message));
|
.then(() => ffLoadDash()).catch(e => alert('램프 시작 실패: ' + e.message));
|
||||||
}
|
}
|
||||||
@@ -313,6 +321,7 @@ function ffCard(c) {
|
|||||||
c.mode === 'Recovering' ? '<span class="ff-mode ff-mode-rec">전환류 복귀중 ●</span>'
|
c.mode === 'Recovering' ? '<span class="ff-mode ff-mode-rec">전환류 복귀중 ●</span>'
|
||||||
: c.mode === 'Returning' ? '<span class="ff-mode ff-mode-ret">복귀 램프 ●</span>'
|
: c.mode === 'Returning' ? '<span class="ff-mode ff-mode-ret">복귀 램프 ●</span>'
|
||||||
: armWait ? '<span class="ff-mode ff-mode-arm">전환류 권장 ⚠</span>'
|
: armWait ? '<span class="ff-mode ff-mode-arm">전환류 권장 ⚠</span>'
|
||||||
|
: c.mode === 'Normal' ? '<span class="ff-mode ff-mode-nrm">정상 ●</span>'
|
||||||
: '';
|
: '';
|
||||||
const recoveryCtl =
|
const recoveryCtl =
|
||||||
armWait ? `<button class="btn sm danger" onclick="ffArm(${c.columnId})">전환류 ARM</button>`
|
armWait ? `<button class="btn sm danger" onclick="ffArm(${c.columnId})">전환류 ARM</button>`
|
||||||
@@ -326,7 +335,7 @@ function ffCard(c) {
|
|||||||
const rampCtl = rampActive ? '' : `<div class="ff-feedramp">FEED Target SP
|
const rampCtl = rampActive ? '' : `<div class="ff-feedramp">FEED Target SP
|
||||||
<input class="inp ff-rt" data-col="${c.columnId}" type="number" step="any" value="${ffRampTargets[c.columnId] ?? ''}"
|
<input class="inp ff-rt" data-col="${c.columnId}" type="number" step="any" value="${ffRampTargets[c.columnId] ?? ''}"
|
||||||
oninput="ffRampTargets[${c.columnId}]=this.value" placeholder="목표">
|
oninput="ffRampTargets[${c.columnId}]=this.value" placeholder="목표">
|
||||||
<button class="btn sm danger" onclick="ffCardRampStart(${c.columnId})" title="목표까지 FEED SP를 램프율로 점진 상승">램프 시작 ▶</button>
|
<button class="btn sm danger" onclick="ffCardRampStart(${c.columnId}, ${c.feedFiltered})" title="목표까지 FEED SP를 램프율로 점진 상승/하강">램프 시작 ▶</button>
|
||||||
<span class="ff-rt-mode">${ffRampDryRun ? '[모의]' : '[실쓰기]'}</span></div>`;
|
<span class="ff-rt-mode">${ffRampDryRun ? '[모의]' : '[실쓰기]'}</span></div>`;
|
||||||
const writeBadge = c.autoWriteActive ? '<span class="ff-write-badge">자동 SP 쓰기</span>' : '';
|
const writeBadge = c.autoWriteActive ? '<span class="ff-write-badge">자동 SP 쓰기</span>' : '';
|
||||||
const wgBlocked = c.writeGuardBlockedSp != null
|
const wgBlocked = c.writeGuardBlockedSp != null
|
||||||
@@ -404,6 +413,9 @@ function ffEditColumn(c) {
|
|||||||
// WO-6 전환류 복귀
|
// WO-6 전환류 복귀
|
||||||
recoveryEnabled:false, recoveryAutoArm:false, imbalanceTriggerFrac:0.10, imbalanceTriggerSec:600,
|
recoveryEnabled:false, recoveryAutoArm:false, imbalanceTriggerFrac:0.10, imbalanceTriggerSec:600,
|
||||||
recoverySettleSec:1800, returnRampSec:600, feedRecoverySp:0, deltaPTag:'', deltaPFloodLimit:1e9, tempHighLimit:1e9,
|
recoverySettleSec:1800, returnRampSec:600, feedRecoverySp:0, deltaPTag:'', deltaPFloodLimit:1e9, tempHighLimit:1e9,
|
||||||
|
// 민감단(T_C) 전환·복귀
|
||||||
|
tempLowLimit:-1e9, tcReturnRebTarget:null, tcReturnRebBand:0.5,
|
||||||
|
tcReturnDeltaAdRef:null, tcReturnDeltaAdBand:0.4, tcReturnTcTarget:null, tcReturnTcBand:1.0,
|
||||||
streams:[
|
streams:[
|
||||||
{key:'P',flowTag:'',role:'Commanded',levelTag:'',targetCoeff:0.95,thetaUpSec:60,thetaDnSec:60,tauSec:900,spMin:0,spMax:9999,rateUpPerMin:30,rateDnPerMin:60,refluxFromProduct:false,grade:'A',isReflux:false,recoverySp:0,spNodeId:''},
|
{key:'P',flowTag:'',role:'Commanded',levelTag:'',targetCoeff:0.95,thetaUpSec:60,thetaDnSec:60,tauSec:900,spMin:0,spMax:9999,rateUpPerMin:30,rateDnPerMin:60,refluxFromProduct:false,grade:'A',isReflux:false,recoverySp:0,spNodeId:''},
|
||||||
{key:'R',flowTag:'',role:'Commanded',levelTag:'',targetCoeff:0.80,thetaUpSec:0,thetaDnSec:0,tauSec:0,spMin:0,spMax:9999,rateUpPerMin:30,rateDnPerMin:30,refluxFromProduct:true,grade:'A',isReflux:true,recoverySp:null,spNodeId:''},
|
{key:'R',flowTag:'',role:'Commanded',levelTag:'',targetCoeff:0.80,thetaUpSec:0,thetaDnSec:0,tauSec:0,spMin:0,spMax:9999,rateUpPerMin:30,rateDnPerMin:30,refluxFromProduct:true,grade:'A',isReflux:true,recoverySp:null,spNodeId:''},
|
||||||
@@ -413,7 +425,10 @@ function ffEditColumn(c) {
|
|||||||
: { ...c, pressureTag: c.pressureTag||'',
|
: { ...c, pressureTag: c.pressureTag||'',
|
||||||
controllerId: c.controllerId||'C1', feedSpNodeId: c.feedSpNodeId||'',
|
controllerId: c.controllerId||'C1', feedSpNodeId: c.feedSpNodeId||'',
|
||||||
feedSpMin: c.feedSpMin==null?0:c.feedSpMin, feedSpMax: c.feedSpMax==null?1e9:c.feedSpMax,
|
feedSpMin: c.feedSpMin==null?0:c.feedSpMin, feedSpMax: c.feedSpMax==null?1e9:c.feedSpMax,
|
||||||
tempTags: c.tempTags||[], sensitiveTrayTag: c.sensitiveTrayTag||'', steamOpTag: c.steamOpTag||'', deltaPTag: c.deltaPTag||'' };
|
tempTags: c.tempTags||[], sensitiveTrayTag: c.sensitiveTrayTag||'', steamOpTag: c.steamOpTag||'', deltaPTag: c.deltaPTag||'',
|
||||||
|
tempLowLimit: c.tempLowLimit??-1e9, tcReturnRebTarget: c.tcReturnRebTarget, tcReturnRebBand: c.tcReturnRebBand??0.5,
|
||||||
|
tcReturnDeltaAdRef: c.tcReturnDeltaAdRef, tcReturnDeltaAdBand: c.tcReturnDeltaAdBand??0.4,
|
||||||
|
tcReturnTcTarget: c.tcReturnTcTarget, tcReturnTcBand: c.tcReturnTcBand??1.0 };
|
||||||
|
|
||||||
const colHtml = `
|
const colHtml = `
|
||||||
<div class="ff-modal-col">
|
<div class="ff-modal-col">
|
||||||
@@ -459,6 +474,14 @@ function ffEditColumn(c) {
|
|||||||
<label><span class="ff-desc">차압(ΔP) 태그: 플러딩 트리거용(선택). 비우면 미사용</span><input class="inp" id="ff-f-deltaPTag" value="${esc(def.deltaPTag||'')}"></label>
|
<label><span class="ff-desc">차압(ΔP) 태그: 플러딩 트리거용(선택). 비우면 미사용</span><input class="inp" id="ff-f-deltaPTag" value="${esc(def.deltaPTag||'')}"></label>
|
||||||
<label><span class="ff-desc">ΔP 플러딩 상한: 초과 지속 시 전환류 트리거. 미사용 시 매우 큰 값</span><input class="inp" type="number" step="any" id="ff-f-deltaPFloodLimit" value="${def.deltaPFloodLimit}"></label>
|
<label><span class="ff-desc">ΔP 플러딩 상한: 초과 지속 시 전환류 트리거. 미사용 시 매우 큰 값</span><input class="inp" type="number" step="any" id="ff-f-deltaPFloodLimit" value="${def.deltaPFloodLimit}"></label>
|
||||||
<label><span class="ff-desc">온도 HIGH LIMIT(raw): 온도태그 중 최고값이 초과 시 전환류 트리거(단독). 미사용 시 매우 큰 값(1e9)</span><input class="inp" type="number" step="any" id="ff-f-tempHighLimit" value="${def.tempHighLimit==null?1e9:def.tempHighLimit}"></label>
|
<label><span class="ff-desc">온도 HIGH LIMIT(raw): 온도태그 중 최고값이 초과 시 전환류 트리거(단독). 미사용 시 매우 큰 값(1e9)</span><input class="inp" type="number" step="any" id="ff-f-tempHighLimit" value="${def.tempHighLimit==null?1e9:def.tempHighLimit}"></label>
|
||||||
|
<div class="ff-modal-subhd">민감단(T_C) 전환·복귀</div>
|
||||||
|
<label><span class="ff-desc">T_C 하한(℃): 민감단 온도가 이 값 이하 시 전환류 트리거. -1e9=비활성</span><input class="inp" type="number" step="any" id="ff-f-tempLowLimit" value="${def.tempLowLimit}"></label>
|
||||||
|
<label><span class="ff-desc">T_C 복귀 목표(℃): ★민감단 전용 목표(reb-A와 다름). 빈칸=게이트 비활성</span><input class="inp" type="number" step="any" id="ff-f-tcReturnTcTarget" value="${def.tcReturnTcTarget==null?'':def.tcReturnTcTarget}"></label>
|
||||||
|
<label><span class="ff-desc">T_C 대역(±℃): T_C 목표 대역 반폭</span><input class="inp" type="number" step="any" id="ff-f-tcReturnTcBand" value="${def.tcReturnTcBand}"></label>
|
||||||
|
<label><span class="ff-desc">reb-A 복귀 목표(℃): 빈칸=게이트 비활성</span><input class="inp" type="number" step="any" id="ff-f-tcReturnRebTarget" value="${def.tcReturnRebTarget==null?'':def.tcReturnRebTarget}"></label>
|
||||||
|
<label><span class="ff-desc">reb-A 대역(±℃): reb-A 목표 대역 반폭</span><input class="inp" type="number" step="any" id="ff-f-tcReturnRebBand" value="${def.tcReturnRebBand}"></label>
|
||||||
|
<label><span class="ff-desc">ΔT(A-D) 기준(℃): 빈칸=게이트 비활성</span><input class="inp" type="number" step="any" id="ff-f-tcReturnDeltaAdRef" value="${def.tcReturnDeltaAdRef==null?'':def.tcReturnDeltaAdRef}"></label>
|
||||||
|
<label><span class="ff-desc">ΔT(A-D) 대역(±℃): 안정 대역 반폭</span><input class="inp" type="number" step="any" id="ff-f-tcReturnDeltaAdBand" value="${def.tcReturnDeltaAdBand}"></label>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
@@ -582,6 +605,13 @@ function ffSaveForm(existingId) {
|
|||||||
deltaPTag: g('ff-f-deltaPTag').value || null,
|
deltaPTag: g('ff-f-deltaPTag').value || null,
|
||||||
deltaPFloodLimit: +g('ff-f-deltaPFloodLimit').value,
|
deltaPFloodLimit: +g('ff-f-deltaPFloodLimit').value,
|
||||||
tempHighLimit: +g('ff-f-tempHighLimit').value,
|
tempHighLimit: +g('ff-f-tempHighLimit').value,
|
||||||
|
tempLowLimit: +g('ff-f-tempLowLimit').value,
|
||||||
|
tcReturnRebTarget: g('ff-f-tcReturnRebTarget').value === '' ? undefined : +g('ff-f-tcReturnRebTarget').value,
|
||||||
|
tcReturnRebBand: +g('ff-f-tcReturnRebBand').value,
|
||||||
|
tcReturnDeltaAdRef: g('ff-f-tcReturnDeltaAdRef').value === '' ? undefined : +g('ff-f-tcReturnDeltaAdRef').value,
|
||||||
|
tcReturnDeltaAdBand: +g('ff-f-tcReturnDeltaAdBand').value,
|
||||||
|
tcReturnTcTarget: g('ff-f-tcReturnTcTarget').value === '' ? undefined : +g('ff-f-tcReturnTcTarget').value,
|
||||||
|
tcReturnTcBand: +g('ff-f-tcReturnTcBand').value,
|
||||||
streams: Array.from(document.querySelectorAll('#ff-stream-body tr')).map(tr => {
|
streams: Array.from(document.querySelectorAll('#ff-stream-body tr')).map(tr => {
|
||||||
const v = (sel, f) => {
|
const v = (sel, f) => {
|
||||||
const el = tr.querySelector(`[data-f="${f}"]`);
|
const el = tr.querySelector(`[data-f="${f}"]`);
|
||||||
|
|||||||
@@ -63,15 +63,20 @@ async function stLoadColumns() {
|
|||||||
const sel1 = document.getElementById('st-col');
|
const sel1 = document.getElementById('st-col');
|
||||||
const sel2 = document.getElementById('st-bt-col');
|
const sel2 = document.getElementById('st-bt-col');
|
||||||
const cols = d.configured || d.columns || [];
|
const cols = d.configured || d.columns || [];
|
||||||
|
const defaultCol = d.defaultColumn || 'C-6111';
|
||||||
[sel1, sel2].forEach(sel => {
|
[sel1, sel2].forEach(sel => {
|
||||||
sel.innerHTML = cols.map(c => `<option value="${c}">${c}</option>`).join('');
|
sel.innerHTML = cols.map(c => `<option value="${c}" ${c===defaultCol?'selected':''}>${c}</option>`).join('');
|
||||||
});
|
});
|
||||||
} catch (_) {}
|
} catch (e) {
|
||||||
|
const msg = document.getElementById('st-live-msg');
|
||||||
|
if (msg) msg.textContent = '⚠ 컬럼 목록 로드 실패: ' + e.message;
|
||||||
|
console.warn('[steam] stLoadColumns fail:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 온도 프로파일 이격 모니터 ── */
|
/* ── 온도 프로파일 이격 모니터 ── */
|
||||||
let stTempTimer = null;
|
let stTempTimer = null;
|
||||||
const ST_TEMP_COLS = [['61','6-1차'],['62','6-2차'],['81','8차'],['91','9-1차'],['92','9-2차'],['101','10-1차'],['102','10-2차']];
|
const ST_TEMP_COLS = [['C-6111','6-1차'],['C-6211','6-2차'],['C-8111','8차'],['C-9111','9-1차'],['C-9211','9-2차'],['C-10111','10-1차'],['C-10211','10-2차']];
|
||||||
const ST_STAGE_LABEL = { reb_temp:'reb-A(보텀)', T_B:'T_B', T_C:'T_C(민감단)', T_D:'T_D(탑)' };
|
const ST_STAGE_LABEL = { reb_temp:'reb-A(보텀)', T_B:'T_B', T_C:'T_C(민감단)', T_D:'T_D(탑)' };
|
||||||
const stFmt = v => (v === null || v === undefined || Number.isNaN(v)) ? '—' : (+v).toFixed(1);
|
const stFmt = v => (v === null || v === undefined || Number.isNaN(v)) ? '—' : (+v).toFixed(1);
|
||||||
|
|
||||||
@@ -164,8 +169,14 @@ async function stLiveTick() {
|
|||||||
const col = document.getElementById('st-col').value;
|
const col = document.getElementById('st-col').value;
|
||||||
try {
|
try {
|
||||||
const d = await api('GET', `/api/steam/live?col=${col}`);
|
const d = await api('GET', `/api/steam/live?col=${col}`);
|
||||||
|
if (d.status === 'missing_tags') {
|
||||||
|
document.getElementById('st-live-msg').textContent = `⚠ 태그 없음: ${d.missing?.join(', ') || '—'} (${d.message || '게이트웨이 폴링 확인'})`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
stUpdateLive(d);
|
stUpdateLive(d);
|
||||||
} catch (_) {}
|
} catch (e) {
|
||||||
|
document.getElementById('st-live-msg').textContent = '⚠ 조회 실패: ' + e.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stUpdateLive(d) {
|
function stUpdateLive(d) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -6,15 +7,34 @@ namespace Hc900Crawler.Infrastructure.Control;
|
|||||||
|
|
||||||
public sealed record SteamModel
|
public sealed record SteamModel
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("column")]
|
||||||
public string Column { get; init; } = "";
|
public string Column { get; init; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("features")]
|
||||||
public List<string> Features { get; init; } = [];
|
public List<string> Features { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("linear_coeffs")]
|
||||||
public List<double> LinearCoeffs { get; init; } = [];
|
public List<double> LinearCoeffs { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("intercept")]
|
||||||
public double Intercept { get; init; }
|
public double Intercept { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("linear_r2")]
|
||||||
public double LinearR2 { get; init; }
|
public double LinearR2 { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("gbm_r2")]
|
||||||
public double? GbmR2 { get; init; }
|
public double? GbmR2 { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("valve_poly")]
|
||||||
public List<double> ValvePoly { get; init; } = []; // c3, c2, c1, c0
|
public List<double> ValvePoly { get; init; } = []; // c3, c2, c1, c0
|
||||||
|
|
||||||
|
[JsonPropertyName("envelope_lo")]
|
||||||
public Dictionary<string, double> EnvelopeLo { get; init; } = [];
|
public Dictionary<string, double> EnvelopeLo { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("envelope_hi")]
|
||||||
public Dictionary<string, double> EnvelopeHi { get; init; } = [];
|
public Dictionary<string, double> EnvelopeHi { get; init; } = [];
|
||||||
|
|
||||||
|
[JsonPropertyName("n_operating_points")]
|
||||||
public int NOperatingPoints { get; init; }
|
public int NOperatingPoints { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +61,7 @@ public sealed class SteamAdvisor
|
|||||||
public SteamAdvisor(IConfiguration config, ILogger<SteamAdvisor> logger)
|
public SteamAdvisor(IConfiguration config, ILogger<SteamAdvisor> logger)
|
||||||
{
|
{
|
||||||
_modelPath = config.GetValue<string>("SteamAdvisor:ModelPath")
|
_modelPath = config.GetValue<string>("SteamAdvisor:ModelPath")
|
||||||
?? "/home/windpacer/projects/hc900_ax/scripts/analysis/c6111_model.json";
|
?? "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_model.json";
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
LoadModel();
|
LoadModel();
|
||||||
}
|
}
|
||||||
@@ -73,6 +93,12 @@ public sealed class SteamAdvisor
|
|||||||
|
|
||||||
var mode = ClassifyMode(feed, product, tC);
|
var mode = ClassifyMode(feed, product, tC);
|
||||||
var inEnv = InEnvelope(feed, product, tC, _model);
|
var inEnv = InEnvelope(feed, product, tC, _model);
|
||||||
|
if (_model.LinearCoeffs.Count < 3)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[SteamAdvisor] LinearCoeffs 부족 ({Count}개, 3 필요)", _model.LinearCoeffs.Count);
|
||||||
|
return new SteamAdvisoryResult { Message = "모델 계수 부족", Confidence = "N/A",
|
||||||
|
Mode = mode, Feed = feed, Product = product, TC = tC };
|
||||||
|
}
|
||||||
var steam = _model.Intercept
|
var steam = _model.Intercept
|
||||||
+ _model.LinearCoeffs[0] * feed
|
+ _model.LinearCoeffs[0] * feed
|
||||||
+ _model.LinearCoeffs[1] * product
|
+ _model.LinearCoeffs[1] * product
|
||||||
|
|||||||
@@ -486,9 +486,28 @@ public class Hc900DbService : IExperionDbService
|
|||||||
""");
|
""");
|
||||||
|
|
||||||
// realtime_table: UNIQUE(controller_id, tagname) for ON CONFLICT upsert
|
// realtime_table: UNIQUE(controller_id, tagname) for ON CONFLICT upsert
|
||||||
|
// Also drop the legacy tagname-only UNIQUE index/constraint that would
|
||||||
|
// conflict with peer-mirrored tags (same tagname, different controller_id).
|
||||||
await _ctx.Database.ExecuteSqlRawAsync("""
|
await _ctx.Database.ExecuteSqlRawAsync("""
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
-- Drop legacy tagname-only unique constraint if present
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conrelid = 'realtime_table'::regclass
|
||||||
|
AND conname = 'realtime_table_tagname_key'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE realtime_table DROP CONSTRAINT realtime_table_tagname_key;
|
||||||
|
END IF;
|
||||||
|
-- Drop legacy tagname-only unique index if present
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'realtime_table'
|
||||||
|
AND indexname = 'realtime_table_tagname_key'
|
||||||
|
) THEN
|
||||||
|
DROP INDEX IF EXISTS realtime_table_tagname_key;
|
||||||
|
END IF;
|
||||||
|
-- Ensure composite unique index exists
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
SELECT 1 FROM pg_indexes
|
SELECT 1 FROM pg_indexes
|
||||||
WHERE tablename = 'realtime_table'
|
WHERE tablename = 'realtime_table'
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ public class ControllerProcessManager : BackgroundService
|
|||||||
if (!string.IsNullOrEmpty(snapshot.Shared.LdLibraryPath))
|
if (!string.IsNullOrEmpty(snapshot.Shared.LdLibraryPath))
|
||||||
psi.EnvironmentVariables["LD_LIBRARY_PATH"] = snapshot.Shared.LdLibraryPath;
|
psi.EnvironmentVariables["LD_LIBRARY_PATH"] = snapshot.Shared.LdLibraryPath;
|
||||||
|
|
||||||
|
psi.EnvironmentVariables["LD_PRELOAD"] =
|
||||||
|
"/home/windpacer/projects/hc900_ax/industrial-comm/cpp/build/sigpipe_ignore.so";
|
||||||
|
|
||||||
var proc = new Process { StartInfo = psi };
|
var proc = new Process { StartInfo = psi };
|
||||||
proc.OutputDataReceived += (_, e) => { if (e.Data != null) AppendLog(logPath, e.Data); };
|
proc.OutputDataReceived += (_, e) => { if (e.Data != null) AppendLog(logPath, e.Data); };
|
||||||
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) AppendLog(logPath, e.Data); };
|
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) AppendLog(logPath, e.Data); };
|
||||||
|
|||||||
Reference in New Issue
Block a user