feat: FF 온도계 위젯 + 민감단 모듈1 T_C 유지 SP 제안
위젯: AdvisoryResult에 config 시각화 필드 추가, ffThermometer() 세로 온도계 위젯 구현 (4단 마커+sweet spot 밴드+하한선+C편차 화살표+옵션 푸터) 모듈1: FeedforwardEngine SteamAdvisor DI + ResolveTcTemp, Normal 모드에서 T_C 유지 Steam OP 제안 (SteamAdvisor Predict + FB trim gain-0.6/dwell20분)
BIN
docs/PG-Dump-20260605/field_full_postgres.dump
Normal file
BIN
docs/PG-Dump-20260605/field_full_shinam.dump
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - II.1 DCS CABINET DRAWING.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - II.2 MCC CABINET DRAWING.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - II.3 DCS CABINET PARTS.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - II.4 MCC CABINET PARTS.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.1 MASS FLOWMETER.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.2 VORTEX METER.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.3 METAL TUBE ROTAMETER.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.4 N2 PURGE METER.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.5 CONTROL VALVE.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.6 Pneumatic ONOFF Valve.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.7 LEVEL TRANSMITTER.pdf
Normal file
BIN
docs/도면-데이터시트/As Built Drawing - III.9 TEMPERATURE SENSOR.pdf
Normal file
BIN
docs/도면-데이터시트/FCV-9101-CV-SHEET.pdf
Normal file
BIN
docs/도면-데이터시트/FCV-9101-DATASHEET.pdf
Normal file
BIN
docs/도면-데이터시트/TCV-9111-CV-SHEET.pdf
Normal file
BIN
docs/도면-데이터시트/TCV-9111-DATASHEET.pdf
Normal file
115
docs/작업플랜-FF온도계위젯-C민감단-sweetspot.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 작업플랜 — FF 카드 세로 온도계 위젯 (T_C sweet spot) (2026-06-07)
|
||||
|
||||
> 상위: `docs/작업지시서-STEAM-SP-FF통합.md` ③단계(UI 행)의 시각화 컴포넌트.
|
||||
> 설계 맥락: SteamAdvisor(steam=f(feed,product,T_C)→OP)를 FF 추종 프레임에 통합 중.
|
||||
|
||||
## 진단 (2026-06-07, diagnosis-checklist.md 적용)
|
||||
|
||||
| # | 심각도 | 항목 | 근거 | 파일·라인 |
|
||||
|---|--------|------|------|-----------|
|
||||
| 1 | 🟠 MED | **위젯이 Config 데이터에 접근할 방법 미정의** | 계획은 `tcReturnTcTarget`/`tcReturnTcBand`(밴드)와 `sensitiveTrayTag`(C 마커 식별)를 config 응답에서 가져온다고 하나, `ffCard(c)`는 오직 `/api/ff/advisory`의 `MapColumn`만 인자로 받음. Config는 별도 `/api/ff/config`로 로드되어 `ff-cfg-list`에만 렌더 — `ffCard` 스코프에 config 데이터가 없다. 밴드 생략(마커만)으로 우회는 가능하지만, C 마커를 tag name 매칭 없이 index 추정(70%)에만 의존하면 컬럼별 TempTags 개수·순서 차이 시 오표시. | `wwwroot/js/ff.js:190` (`ffCard(c)` 호출, `c`는 advisory 전용), 대비 `FeedforwardController.cs:381-459` (MapColumn에 config 필드 없음), 계획 §단계-1 |
|
||||
| 2 | 🟡 LOW | **temps 순서 가정이 TempTags 설정 순서에 의존** | 계획(line 36)은 `reb-A=temps[0]`, `D=temps[n-1]`를 가정하나, 이 순서는 DB `TempTags` 배열 순서에 전적으로 의존. `BuildTemps`(FeedforwardEngine.cs:177)는 주석으로 "하단→상단 순서"를 전제하지만 강제하지 않음. 운전자/엔지니어가 설정 화면에서 태그 순서를 잘못 입력하면 마커 라벨이 온도와 불일치. 계획(line 72-73)이 "향후 config화"로 명시하나 first-cut에서도 조치 필요. | `FeedforwardEngine.cs:177-205` (BuildTemps, 순서 검증 없음), 계획 §현자산 line 36, §주의 line 72-73 |
|
||||
| 3 | 🟡 LOW | **폴링 주기 불일치 (5s vs 3s)** | 검증기준(line 64)은 "5초 폴링마다"라고 명시했으나 `ffInit`(ff.js:25)는 `setInterval(ffLoadDash, 3000)`으로 3초. 온도계 갱신 주기도 동일 타이머를 공유하므로 문서와 실제 동작 간 차이 발생. 기능상 문제는 없으나 혼선 가능. | 계획 §검증기준 line 64, 대비 `ff.js:25` |
|
||||
| 4 | 🟡 LOW | **C-index 70% fallback이 4-Temp 가정에 묶임** | 계획(line 51)은 `sensitiveTrayTag` 매칭 실패 시 "인덱스 추정(70%)"으로 C를 식별. C-6111은 4개 temps(index=2로 정확)이나, 3개 temps면 index=2(마지막), 5개면 index=3(80% 위치)으로 의도한 민감단과 다를 수 있음. `sensitiveTrayTag`가 config에 있고 advisory에 없으므로 사실상 항상 fallback에 의존할 가능성. | 계획 §단계-1 line 51, `FeedforwardController.cs:450-456` (MapColumn temps에 tag name 포함 — 매칭 가능은 하나 config 필드 없음) |
|
||||
|
||||
### 교차검증 종합
|
||||
|
||||
- Q1 (이미 수정됨?) — 해당 없음 (plan 문서, 아직 구현 전)
|
||||
- Q2 (다른 레이어 처리?) — #1: advisory 응답에 `sensitiveTrayTag`를 추가하거나, ffLoadDash가 config를 병렬 로드해 ffCard에 넘겨야 함. 현재 어느 쪽도 아님.
|
||||
- Q3 (의도적 설계?) — #1은 설계 누락(구현 시 발견될 문제). #2·#4는 계획이 인지한 범위 내 한계. #3은 단순 오기.
|
||||
- Q4 (재현 시나리오?) — #1: ffThermometer 구현 시 sweet spot 밴드 미표시 또는 C 마커 오식별. #2: TempTags 순서 실수 시 마커 라벨 혼동.
|
||||
|
||||
### 연계 진단 — `작업플랜-민감단온도-전환복귀제어.md`와의 통합
|
||||
|
||||
| # | 심각도 | 항목 | 근거 | 관련 Plan |
|
||||
|---|--------|------|------|-----------|
|
||||
| 5 | 🟠 MED | **`tcReturnTcTarget/Band`가 시각화와 제어게이트를 겸함 — 안전 문제** | 위젯은 `tcReturnTcTarget ± tcReturnTcBand`를 sweet spot 시각화 밴드로 사용(계획 line 37-38). 동시에 엔진 `ApplyRecovery()`(FeedforwardEngine.cs:497-498)는 같은 필드를 **복귀(Returning) 게이트 조건**으로 평가함. 운전자가 시각화 편의를 위해 밴드를 넓히면 복귀 게이트가 느슨해져 T_C가 완전히 안정되지 않았는데 Normal 복귀 허용 위험. 두 용도가 동일 config 필드를 공유하는 것은 설계상 충돌. | 민감단 §모듈3 line 57, FF위젯 §현자산 line 37-38, `FeedforwardEngine.cs:497-498` |
|
||||
| 6 | 🟠 MED | **위젯에 `TempLowLimit` 미반영 — T_C 하한 경고 누락** | 엔진은 `TempLowLimit`(기본 -1e9=비활성)으로 T_C 하한 이탈을 감시(sigTLow, FeedforwardEngine.cs:444), 전환류 진입 트리거로 사용. 위젯은 sweet spot 밴드만 표시하고 `TempLowLimit` 경계를 표시하지 않음. `TempLowLimit`이 `tcReturnTcTarget - tcReturnTcBand`보다 높게 설정된 경우(예: 목표±1℃ 대신 하한 -3℃), C가 sweet spot 밴드 안에 있어 초록이지만 실제로는 하한 임박 — 운전자 오인 유발. 위젯 온도계에 하한 임계선(적색 점선)을 추가하거나, 최소한 위험 상태 배지 연동 필요. | 민감단 §모듈2 line 51, FF위젯 §단계-2 line 55-56, `FeedforwardEngine.cs:444` |
|
||||
| 7 | 🟡 LOW | **`EnteredByTcLow`가 advisory에 미노출 — 전환류 원인 표시 불가** | 엔진은 `st.EnteredByTcLow`(FeedforwardEngine.cs:46)로 전환류 진입이 T_C 하한 때문인지 기록하고, 복귀 게이트 분기(같은 파일 line 502)에 사용. 이 정보가 `AdvisoryResult`와 `MapColumn`에 없어 위젯이 "T_C 하한 진입" vs "물질수지/ΔP 진입"을 구분해 표시할 수 없음. | 민감단 §큰그림, `FeedforwardEngine.cs:46/469/502`, `FeedforwardController.cs:421-459` (MapColumn) |
|
||||
| 8 | 🟡 LOW | **`TempLowLimit`도 advisory에 미노출 — 위젯이 하한 임계선 표시 불가** | `MapColumn`(FeedforwardController.cs:421-459)에 `tempLowLimit` 누락. 위젯이 하한 임계선을 그리려면 이 값이 필요. `tempLowLimit`은 엔진에서 읽지만 advisory 응답에 포함되지 않음. #6 해결의 선행조건. | `FeedforwardController.cs:421-459` (MapColumn), `ColumnConfig.TempLowLimit` |
|
||||
| 9 | 🟡 LOW | **온도계 초록(정상)과 모드배지 간 시차 혼동 가능** | 위젯은 T_C가 밴드 안이면 즉시 초록(◉) 표시. 엔진은 Returning 게이트 조건(T_C in-band + reb-A in-band + ΔT(A-D) 안정)이 `RecoverySettleSec`(기본 1800s=30분) 지속되어야 Normal로 전이(FeedforwardEngine.cs:504). 따라서 운전자는 온도계는 초록인데 모드배지는 "전환류 평형대기 1200/1800s"인 상황을 30분간 보게 됨. 정상 동작이나 사전 안내 없으면 혼란. | 민감단 §모듈3 line 57-58, `ff.js:320-325` (모드배지), `FeedforwardEngine.cs:497-504` |
|
||||
| 10 | 🟡 LOW | **온도계 마커가 Recovering 중에도 살아있어야 하나 미명시** | 전환류(Recovering) 중에도 T_C 온도는 유효하며, 운전자는 T_C가 회복되는 과정을 실시간으로 봐야 복귀 게이트 진행을 납득 가능. 위젯 계획(line 76)은 "표시 전용"이라 명시했으나 Recovering 중에도 위젯이 활성 상태를 유지하는지 미기재. `c.temps`가 Recovering 중에도 정상 제공되므로 별도 처리 불필요하나, 명시적 언급이 없으면 구현 시 실수로 숨길 가능성. | FF위젯 §주의 line 76, §단계-1 line 51 (c.temps 조건부 표시) |
|
||||
| 11 | 🟡 LOW | **하강 램프 시각화 미지원** | 민감단 계획(§4-A)은 FeedRampAdvisor 하강 램프 추가를 명시하나, `FeedRampCalculator.Compute()`(FeedRampCalculator.cs:87-88)는 아직 하강 미구현. 위젯은 이와 무관하지만, 램프 진행 중 온도계가 어떻게 변화하는지(특히 하강 램프가 T_C에 미치는 영향)를 보여주지 않으면 운전자 피드백 루프 불완전. 단, FF위젯 계획이 "표시 전용"이므로 직접 구현 범위 밖 — 인지하고 넘어갈 것. | 민감단 §4-A line 62, `FeedRampCalculator.cs:87-88` |
|
||||
|
||||
### 연계 교차검증
|
||||
|
||||
- Q1 (이미 수정됨?) — #11의 하강 램프는 `FeedRampCalculator`에 여전히 미구현 상태 (Q1 통과)
|
||||
- Q2 (다른 레이어 처리?) — #5: 두 plan 모두 동일 config 필드를 각자 용도로 참조하므로, 다른 레이어에서 해소되지 않음. #7·#8: advisory DTO가 누락 — MapColumn에 필드 추가 필요.
|
||||
- Q3 (의도적 설계?) — #5: 의도적이 아니라 간과된 설계 충돌. #9: 정상 동작이나 UX 관점에서 명시 필요. #10: 위젯 계획에 명시적 언급 없음.
|
||||
- Q4 (재현 시나리오?) — #5: 운전자가 설정 화면에서 밴드를 1.0→2.0으로 늘리면, 복귀 게이트가 T_C 편차 ±1℃→±2℃로 완화되어 T_C 미회복 상태에서 Normal 조기 복귀 가능. #6: `TempLowLimit`=207℃, sweet spot=209±1℃일 때 T_C=207.5℃는 밴드 밖이지만 위젯이 하한 임계를 표시하지 않아 "노랑(▲)"만 보고 운전자 대응 늦어짐. #9: 초록 온도계 + "전환류 중" 배지를 30분간 보며 혼란.
|
||||
|
||||
## 목적
|
||||
유량권장(FF) C-6111 카드 **옆에 세로 온도계(thermometer) 위젯**을 붙여,
|
||||
민감단 온도 T_C가 **제품별 목표(설정값) ± 허용대역(sweet spot)** 대비
|
||||
처지는지(▼) / 올라가는지(▲)를 운전자가 한눈에 보게 한다.
|
||||
reb-A·B·C·D 4단을 같은 온도축에 함께 표시해 컬럼 단면 프로파일까지 한 그림에 담는다.
|
||||
|
||||
## 선택된 형태 (결정됨)
|
||||
**세로 온도계 스냅샷** (트렌드 막대/스크롤 아님 — steam 패널 uPlot 트렌드와 중복 회피).
|
||||
- 세로축 = 온도(℃). "처짐/상승" = 마커의 아래/위 이동으로 직결.
|
||||
- sweet spot = 반투명 두꺼운 밴드(목표 ± 허용). 밴드 안에 C가 있으면 정상.
|
||||
- reb-A(0%·bottom)·B(50%)·C(70%·민감단)·D(90%·top) 4개 마커. 각 단 라벨에 % 표기.
|
||||
- C가 밴드 하한 아래로 처지면 ▼ 적색, 상한 위면 ▲ 주황, 안이면 초록.
|
||||
- (옵션 푸터) 신뢰도 + 권장 TICA SP — 백엔드 공백 해소 후.
|
||||
|
||||
```
|
||||
T_C 온도계 (℃)
|
||||
222 ┤◀ reb-A 0%(bottom)
|
||||
213 ┤◀ B 50%
|
||||
░░░░░ 211 sweet 상한
|
||||
░░◉░░ 209 ◀ C 70% ★목표
|
||||
░░░░░ 207 sweet 하한
|
||||
201 ┤◀ D 90%(top)
|
||||
─────────────────
|
||||
conf HIGH · TICA SP→209 ← (옵션, 백엔드 공백)
|
||||
```
|
||||
|
||||
## 현 자산 (재활용, 중복금지)
|
||||
프론트(`/api/ff/advisory` + `/api/ff/config`)에 이미 도달하는 데이터:
|
||||
- **현재 단별 온도** — `c.temps[]` = `{tag, raw, pct, good}`, **하단→상단 순서**
|
||||
(FeedforwardEngine.BuildTemps 주석: `tica-6111a(0%), ti-6111b, ti-6111c(민감단), ti-6111d(top)`).
|
||||
→ reb-A=`temps[0]`, C=민감단=`sensitiveTrayTag` 매칭(또는 70% 위치), D=`temps[n-1]`.
|
||||
- **sweet spot 밴드** — `/api/ff/config`의 `tcReturnTcTarget`(목표℃) ± `tcReturnTcBand`(반폭, 기본 1.0)
|
||||
(FeedforwardController.cs:222-223에서 이미 직렬화). 운전자 입력 설정값 = 이 값.
|
||||
- **상태색 보조** — `c.tempProfileState`, `c.tempSpan`.
|
||||
- 카드 렌더 진입점: `wwwroot/js/ff.js` `ffCard(c)` (257행~). CSS: 동일 ff 스타일시트.
|
||||
|
||||
## 백엔드 공백 (옵션 푸터에만 필요) ⚠
|
||||
스팀 제안(`SteamAdvisor.Predict` → `RecOp/RecSteam/Confidence/Ood`)이
|
||||
**FF advisory DTO(`MapColumn`)에 아직 미노출** (Feedforward 폴더 grep 0건).
|
||||
- 온도계 본체(밴드+4마커+처짐/상승색)는 **공백과 무관하게 1차 구현 가능**.
|
||||
- 푸터의 "conf · TICA SP→" 표시는 작업지시서-STEAM-SP-FF통합 ①단계
|
||||
(`AdvisoryResult`에 Steam 필드 추가 → `MapColumn`에 노출)가 선행돼야 함.
|
||||
|
||||
## 단계
|
||||
1. **데이터 셀렉터 (ff.js)** — `c` 한 개에서 온도계 입력 추출:
|
||||
- `temps = c.temps`(없으면 위젯 미표시), C = `sensitiveTrayTag`로 매칭하되 실패 시 인덱스 추정(70%).
|
||||
- 밴드 = config의 `tcReturnTcTarget`/`tcReturnTcBand`(없으면 밴드 생략, 마커만).
|
||||
- 스케일 = `[min(저온, 밴드하한), max(고온, 밴드상한)]` + 여유 5% 패딩.
|
||||
2. **렌더 (ff.js)** — `ffThermometer(c)` 함수 신설. SVG 또는 div+CSS 세로 막대.
|
||||
- 밴드 사각형(반투명), 4단 마커(틱+라벨+값), C 강조(◉).
|
||||
- C 위치 판정 → 클래스 `ff-tc-low/ok/high`로 색/화살표.
|
||||
3. **카드 결합 (ff.js)** — `ffCard` 반환 마크업에 `ffThermometer(c)`를 카드 우측 컬럼으로 삽입
|
||||
(기존 카드를 flex 2열: 좌=표/배지, 우=온도계).
|
||||
4. **CSS** — `.ff-thermo`(세로 트랙), `.ff-thermo-band`, `.ff-thermo-mark`, `.ff-tc-low/ok/high`.
|
||||
live 갱신 시 `top` 트랜지션으로 마커가 부드럽게 이동(0.3s).
|
||||
5. **(옵션) 푸터** — ①단계 백엔드 노출 후 `conf` + `recSteam`/`recOp` 한 줄. 없으면 숨김.
|
||||
|
||||
## 검증기준
|
||||
- C-6111 카드 옆에 온도계 표시, 5초 폴링마다 4단 마커가 live 이동.
|
||||
- T_C가 밴드 안 → 초록·◉ 밴드 중앙 근처. 밴드 아래로 내리면 ▼ 적색, 위면 ▲ 주황.
|
||||
- `tcReturnTcTarget` 미설정 컬럼 → 밴드 없이 마커만(에러 없음).
|
||||
- `c.temps` 없음/stale → 위젯 graceful 미표시 또는 회색.
|
||||
- (옵션) ①단계 적용 시 푸터에 conf+SP 표시, 미적용 시 푸터 숨김.
|
||||
|
||||
## 주의
|
||||
- range는 realtime/config live값 사용 — xlsx·하드코딩 금지(메모리·작업지시서 일관).
|
||||
- 단 라벨 %(0/50/70/90)는 **C-6111 레이아웃 기준 표시값**. 컬럼별 상이 시 향후 config화
|
||||
(현재 BuildTemps 주석도 C-6111 고정 — 일반화는 별도 작업).
|
||||
- reb-A가 온도축 최상단(최고온)에 오는 건 정상(물리 높이가 아닌 온도축이므로). 라벨로 명시.
|
||||
- 6차만 C3 online → first-cut은 C-6111. 9·10차(C4 미연결)는 데이터 도달 시 자동 적용.
|
||||
- closed-loop 강제·자동쓰기와 무관한 **표시 전용** 위젯(메모리: 조건 실변동중 — 시각화만).
|
||||
|
||||
## 권장순서
|
||||
①셀렉터+렌더(공백 무관) → ②CSS → ③카드결합 → ④(백엔드 ①단계 후)푸터.
|
||||
①~③은 STEAM-SP-FF통합 백엔드와 독립 병행 가능.
|
||||
177815
scripts/analysis/C-10111_plotdata.json
Normal file
15996
scripts/analysis/C-10211_plotdata.json
Normal file
31767
scripts/analysis/C-6111_plotdata.json
Normal file
11635
scripts/analysis/C-6211_plotdata.json
Normal file
9073
scripts/analysis/C-8111_plotdata.json
Normal file
98631
scripts/analysis/C-9111_plotdata.json
Normal file
94626
scripts/analysis/C-9211_plotdata.json
Normal file
BIN
scripts/analysis/c101_advisory.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
scripts/analysis/c101_data.pkl
Normal file
BIN
scripts/analysis/c101_prodmap.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
scripts/analysis/c101_shadow.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
scripts/analysis/c101_shutdown.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
scripts/analysis/c101_startup.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
scripts/analysis/c102_advisory.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
scripts/analysis/c102_data.pkl
Normal file
BIN
scripts/analysis/c102_prodmap.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
scripts/analysis/c102_shadow.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
scripts/analysis/c102_shutdown.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
scripts/analysis/c102_startup.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
scripts/analysis/c51_data.pkl
Normal file
BIN
scripts/analysis/c6111_advisory.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
scripts/analysis/c6111_data.pkl
Normal file
BIN
scripts/analysis/c6111_episode.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
scripts/analysis/c6111_prodmap.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
scripts/analysis/c6111_rolling.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
scripts/analysis/c6111_shadow.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
scripts/analysis/c6111_shutdown.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
scripts/analysis/c6111_startup.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
scripts/analysis/c6111_timeline.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
scripts/analysis/c6111_trim.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
scripts/analysis/c61_advisory.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
scripts/analysis/c61_data.pkl
Normal file
BIN
scripts/analysis/c61_prodmap.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
scripts/analysis/c61_rolling.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
scripts/analysis/c61_shadow.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
scripts/analysis/c61_shutdown.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
scripts/analysis/c61_startup.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
scripts/analysis/c62_advisory.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
scripts/analysis/c62_daily_shadow.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
scripts/analysis/c62_data.pkl
Normal file
BIN
scripts/analysis/c62_prodmap.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
scripts/analysis/c62_rolling.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
scripts/analysis/c62_shadow.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
scripts/analysis/c62_shutdown.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
scripts/analysis/c62_startup.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
scripts/analysis/c81_advisory.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
scripts/analysis/c81_data.pkl
Normal file
BIN
scripts/analysis/c81_prodmap.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
scripts/analysis/c81_rolling.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
scripts/analysis/c81_shadow.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
scripts/analysis/c81_shutdown.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
scripts/analysis/c81_startup.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
scripts/analysis/c91_advisory.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
scripts/analysis/c91_data.pkl
Normal file
BIN
scripts/analysis/c91_prodmap.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
scripts/analysis/c91_rolling.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
scripts/analysis/c91_shadow.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
scripts/analysis/c91_shutdown.png
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
scripts/analysis/c91_startup.png
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
scripts/analysis/c92_advisory.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
scripts/analysis/c92_data.pkl
Normal file
BIN
scripts/analysis/c92_prodmap.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
scripts/analysis/c92_rolling.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
scripts/analysis/c92_shadow.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
scripts/analysis/c92_shutdown.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
scripts/analysis/c92_startup.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
@@ -144,11 +144,20 @@ public sealed record AdvisoryResult(
|
||||
public string? LowerFrontState { get; init; } // 하부 ΔT(C−B): 정상/상승/하강/정착대기
|
||||
public double? UpperFrontMetric { get; init; }
|
||||
public double? LowerFrontMetric { get; init; }
|
||||
public double? FeedRecommendedSp { get; init; } // WO-6 전환류 시 FEED 권장(0=차단), 그 외 null
|
||||
public double? FeedRecommendedSp { get; init; } // WO-6 전환류 시 FEED 권장(0=차단), 그 외 null
|
||||
// Phase II: auto-write 상태
|
||||
public bool AutoWriteActive { get; init; }
|
||||
public double? WriteGuardBlockedSp { get; init; } // WriteGuard가 차단한 SP값(표시용)
|
||||
public string? WriteGuardReason { get; init; } // 차단 사유
|
||||
// 작업플랜-FF온도계위젯: 위젯 렌더용 config 필드 (advisory에 함께 전달)
|
||||
public string? SensitiveTrayTag { get; init; } // 민감트레이 태그명
|
||||
public double? TcReturnTcTarget { get; init; } // sweet spot 목표온도(℃)
|
||||
public double? TcReturnTcBand { get; init; } // sweet spot 반폭(℃)
|
||||
public double? TempLowLimit { get; init; } // T_C 하한 임계(℃)
|
||||
// 작업플랜-민감단온도 모듈1: T_C 유지 Steam OP 제안
|
||||
public double? SteamRecOp { get; init; } // 권장 Steam OP(%)
|
||||
public string? SteamConfidence { get; init; } // HIGH/LOW_OOD/N/A
|
||||
public double? TcDeviation { get; init; } // T_C 편차(℃) = actual - target
|
||||
}
|
||||
|
||||
public sealed record TempPoint(string Tag, double Raw, double Pct, bool Good);
|
||||
|
||||
@@ -447,6 +447,13 @@ public sealed class FeedforwardController : ControllerBase
|
||||
autoWriteActive = r.AutoWriteActive,
|
||||
writeGuardBlockedSp = r.WriteGuardBlockedSp,
|
||||
writeGuardReason = r.WriteGuardReason,
|
||||
sensitiveTrayTag = r.SensitiveTrayTag,
|
||||
tcReturnTcTarget = r.TcReturnTcTarget,
|
||||
tcReturnTcBand = r.TcReturnTcBand,
|
||||
tempLowLimit = r.TempLowLimit,
|
||||
steamRecOp = r.SteamRecOp,
|
||||
steamConfidence = r.SteamConfidence,
|
||||
tcDeviation = r.TcDeviation,
|
||||
temps = r.Temps?.Select(t => new
|
||||
{
|
||||
tag = t.Tag,
|
||||
|
||||
@@ -137,3 +137,26 @@
|
||||
/* WP5 3단계: 조성 입력 폼 */
|
||||
.ff-comp-body { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
.ff-comp-list { width:100%; margin-top:4px; font-size:12px; color:#94a3b8; }
|
||||
|
||||
/* ── FF T_C 온도계 위젯 (sweet spot thermometer) ──────────────── */
|
||||
.ff-has-thermo{display:flex;flex-direction:row;gap:0}
|
||||
.ff-card-main{flex:1;min-width:0}
|
||||
.ff-thermo{display:flex;flex-direction:column;width:110px;flex-shrink:0;border-left:1px solid var(--bd);padding:4px 0 4px 8px}
|
||||
.ff-thermo-hd{font-size:10px;font-weight:700;color:var(--t2);text-align:center;margin-bottom:2px;letter-spacing:-.3px}
|
||||
.ff-thermo-body{flex:1;position:relative;min-height:180px}
|
||||
.ff-thermo-track{position:absolute;inset:4px 0 4px 28px}
|
||||
.ff-thermo-tick{position:absolute;left:-28px;right:0;height:0;border-top:1px solid rgba(255,255,255,.08);pointer-events:none}
|
||||
.ff-thermo-tick span{position:absolute;right:calc(100% + 3px);top:-6px;font-size:9px;color:var(--t3);white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||
.ff-thermo-band{position:absolute;left:0;right:4px;background:rgba(100,200,100,.13);border:1px solid rgba(100,200,100,.25);border-radius:2px;pointer-events:none}
|
||||
.ff-thermo-limit{position:absolute;left:-4px;right:0;height:0;border-top:2px dashed #ff5252;pointer-events:none}
|
||||
.ff-thermo-mark{position:absolute;left:0;right:0;height:0;display:flex;align-items:center;gap:3px;pointer-events:none;margin-top:-6px}
|
||||
.ff-thermo-dot{display:flex;align-items:center;justify-content:center;width:10px;height:10px;font-size:6px;color:var(--t2);background:var(--bg2);border:1px solid var(--t3);border-radius:50%;line-height:1;margin-left:2px;z-index:1}
|
||||
.ff-thermo-c{width:16px;height:16px;font-size:12px;font-weight:700;background:var(--bg1);border-width:2px;z-index:2}
|
||||
.ff-thermo-c.ff-tc-low{border-color:#ff5252;color:#ff5252}
|
||||
.ff-thermo-c.ff-tc-ok{border-color:#69f0ae;color:#69f0ae}
|
||||
.ff-thermo-c.ff-tc-high{border-color:#ffb74d;color:#ffb74d}
|
||||
.ff-thermo-lbl{display:flex;flex-direction:column;gap:0;font-size:10px;color:var(--t0);line-height:1.2;white-space:nowrap}
|
||||
.ff-thermo-lbl small{font-size:8px;color:var(--t3);line-height:1}
|
||||
.ff-thermo-arrow{text-align:center;font-size:16px;line-height:1;padding:1px 0}
|
||||
.ff-thermo-ft{font-size:9px;color:var(--t2);text-align:center;line-height:1.3;padding:2px 0;border-top:1px solid var(--bd);margin-top:2px}
|
||||
.ff-thermo-ft .ff-tc-ok{color:#69f0ae}.ff-thermo-ft .ff-tc-low{color:#ff5252}
|
||||
|
||||
@@ -254,6 +254,82 @@ function ffCardRampStart(colId, currentFeed) {
|
||||
.then(() => ffLoadDash()).catch(e => alert('램프 시작 실패: ' + e.message));
|
||||
}
|
||||
|
||||
// ── FF 온도계 위젯 (T_C sweet spot) ──────────────────────────────
|
||||
function ffThermometer(c) {
|
||||
const temps = c.temps;
|
||||
if (!temps || temps.length < 2) return '';
|
||||
const good = temps.filter(t => t.good && t.raw != null);
|
||||
if (good.length < 2) return '';
|
||||
|
||||
const tcTarget = c.tcReturnTcTarget;
|
||||
const tcBand = c.tcReturnTcBand || 1;
|
||||
const tll = c.tempLowLimit;
|
||||
const cTag = c.sensitiveTrayTag;
|
||||
const cKey = cTag ? (cTag.endsWith('.pv') ? cTag : cTag + '.pv') : '';
|
||||
|
||||
let cIdx = cKey ? temps.findIndex(t => t.tag === cKey) : -1;
|
||||
if (cIdx < 0) cIdx = Math.round((temps.length - 1) * 0.7);
|
||||
cIdx = Math.min(cIdx, temps.length - 1);
|
||||
|
||||
const vals = good.map(t => t.raw);
|
||||
let lo = Math.min(...vals), hi = Math.max(...vals);
|
||||
if (tcTarget != null) { lo = Math.min(lo, tcTarget - tcBand); hi = Math.max(hi, tcTarget + tcBand); }
|
||||
if (tll != null) lo = Math.min(lo, tll);
|
||||
const pad = (hi - lo) * 0.08 || 2; lo -= pad; hi += pad;
|
||||
const range = hi - lo || 1;
|
||||
const toY = v => Math.max(0, Math.min(100, (1 - (v - lo) / range) * 100));
|
||||
|
||||
const bandHtml = tcTarget != null
|
||||
? `<div class="ff-thermo-band" style="top:${toY(tcTarget + tcBand)}%;height:${toY(tcTarget - tcBand) - toY(tcTarget + tcBand)}%"></div>`
|
||||
: '';
|
||||
|
||||
const limitHtml = tll != null
|
||||
? `<div class="ff-thermo-limit" style="top:${toY(tll)}%"></div>` : '';
|
||||
|
||||
const trayLabels = ['reb-A', 'B', 'C', 'D'];
|
||||
const trayPcts = ['0%', '50%', '70%', '90%'];
|
||||
const markers = temps.map((t, i) => {
|
||||
if (!t.good || t.raw == null) return '';
|
||||
const y = toY(t.raw);
|
||||
const isC = i === cIdx;
|
||||
const label = trayLabels[i] || 'T' + i;
|
||||
const pct = trayPcts[i] || '';
|
||||
const cState = isC && tcTarget != null
|
||||
? (t.raw < tcTarget - tcBand ? 'low' : t.raw > tcTarget + tcBand ? 'high' : 'ok') : '';
|
||||
const dotCls = isC
|
||||
? 'ff-thermo-dot ff-thermo-c' + (cState ? ' ff-tc-' + cState : '')
|
||||
: 'ff-thermo-dot';
|
||||
return `<div class="ff-thermo-mark" style="top:${y}%"><span class="${dotCls}">${isC?'◉':'●'}</span><span class="ff-thermo-lbl">${fmtVal(t.raw)}<small> ${label}${pct?' '+pct:''}</small></span></div>`;
|
||||
}).join('');
|
||||
|
||||
const cState = (cIdx >= 0 && temps[cIdx]?.good && temps[cIdx].raw != null && tcTarget != null)
|
||||
? (temps[cIdx].raw < tcTarget - tcBand ? 'low' : temps[cIdx].raw > tcTarget + tcBand ? 'high' : 'ok')
|
||||
: '';
|
||||
const arrow = cState === 'low' ? '▼' : cState === 'high' ? '▲' : '';
|
||||
const arrowCls = 'ff-thermo-arrow' + (cState ? ' ff-tc-' + cState : '');
|
||||
|
||||
// 옵션 푸터: Module 1 T_C 유지 SP 제안
|
||||
const footer = (c.steamRecOp != null)
|
||||
? `<div class="ff-thermo-ft">conf ${esc(c.steamConfidence||'')} · SP→${fmtVal(c.steamRecOp)}${c.tcDeviation!=null?` <span class="ff-tc-${Math.abs(c.tcDeviation)>1?'low':Math.abs(c.tcDeviation)>0.1?'':'ok'}">ε${fmtVal(c.tcDeviation)}</span>`:''}</div>`
|
||||
: '';
|
||||
|
||||
return `<div class="ff-thermo">
|
||||
<div class="ff-thermo-hd">T_C (℃)</div>
|
||||
<div class="ff-thermo-body"><div class="ff-thermo-track">
|
||||
<div class="ff-thermo-tick" style="top:0%"><span>${fmtVal(hi)}</span></div>
|
||||
<div class="ff-thermo-tick" style="top:25%"><span>${fmtVal(hi-(hi-lo)*0.25)}</span></div>
|
||||
<div class="ff-thermo-tick" style="top:50%"><span>${fmtVal(lo+(hi-lo)*0.5)}</span></div>
|
||||
<div class="ff-thermo-tick" style="top:75%"><span>${fmtVal(lo+(hi-lo)*0.25)}</span></div>
|
||||
<div class="ff-thermo-tick" style="top:100%"><span>${fmtVal(lo)}</span></div>
|
||||
${bandHtml}
|
||||
${limitHtml}
|
||||
${markers}
|
||||
</div></div>
|
||||
${arrow ? `<div class="${arrowCls}">${arrow}</div>` : ''}
|
||||
${footer}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function ffCard(c) {
|
||||
const rows = (c.streams || []).map(s => {
|
||||
const lvlTag = s.levelTag || '';
|
||||
@@ -337,32 +413,37 @@ function ffCard(c) {
|
||||
oninput="ffRampTargets[${c.columnId}]=this.value" placeholder="목표">
|
||||
<button class="btn sm danger" onclick="ffCardRampStart(${c.columnId}, ${c.feedFiltered})" title="목표까지 FEED SP를 램프율로 점진 상승/하강">램프 시작 ▶</button>
|
||||
<span class="ff-rt-mode">${ffRampDryRun ? '[모의]' : '[실쓰기]'}</span></div>`;
|
||||
const thermo = ffThermometer(c);
|
||||
const writeBadge = c.autoWriteActive ? '<span class="ff-write-badge">자동 SP 쓰기</span>' : '';
|
||||
const wgBlocked = c.writeGuardBlockedSp != null
|
||||
? `<div class="ff-wg-blocked">쓰기 차단: ${esc(c.writeGuardReason)} (SP <b>${fmtVal(c.writeGuardBlockedSp)}</b>)</div>`
|
||||
: '';
|
||||
const hasThermo = !!thermo;
|
||||
return `
|
||||
<div class="ff-col-card ${c.enabled?'':'ff-disabled'}">
|
||||
<div class="ff-col-head"><b>${esc(c.columnName)}</b>
|
||||
<span class="ff-feed">FEED ${fmtVal(c.feedFiltered)}</span>
|
||||
${writeBadge}
|
||||
<span class="ff-time">${fmtTs(c.computedAt)}</span></div>
|
||||
${modeLine}
|
||||
${rampLine}
|
||||
${rampCtl}
|
||||
${banner}
|
||||
${wgBlocked}
|
||||
<table class="ff-tbl"><thead><tr>
|
||||
<th>스트림</th><th>태그</th><th>역할</th><th>PV</th><th>권장 SP</th><th>Δ</th><th>추세</th><th>신뢰</th>
|
||||
</tr></thead><tbody>${rows}</tbody></table>
|
||||
<div class="ff-mb">${esc(mb)}</div>
|
||||
${temps}
|
||||
${theta}
|
||||
${front}
|
||||
${tpBadge}
|
||||
${front2}
|
||||
${comp}
|
||||
<div class="ff-note">LevelDriven(D·B)은 레벨 제어(LIC)가 SP를 결정. 권장값은 참고 — 인가는 운전원.</div>
|
||||
<div class="ff-col-card ${c.enabled?'':'ff-disabled'} ${hasThermo?'ff-has-thermo':''}">
|
||||
<div class="ff-card-main">
|
||||
<div class="ff-col-head"><b>${esc(c.columnName)}</b>
|
||||
<span class="ff-feed">FEED ${fmtVal(c.feedFiltered)}</span>
|
||||
${writeBadge}
|
||||
<span class="ff-time">${fmtTs(c.computedAt)}</span></div>
|
||||
${modeLine}
|
||||
${rampLine}
|
||||
${rampCtl}
|
||||
${banner}
|
||||
${wgBlocked}
|
||||
<table class="ff-tbl"><thead><tr>
|
||||
<th>스트림</th><th>태그</th><th>역할</th><th>PV</th><th>권장 SP</th><th>Δ</th><th>추세</th><th>신뢰</th>
|
||||
</tr></thead><tbody>${rows}</tbody></table>
|
||||
<div class="ff-mb">${esc(mb)}</div>
|
||||
${temps}
|
||||
${theta}
|
||||
${front}
|
||||
${tpBadge}
|
||||
${front2}
|
||||
${comp}
|
||||
<div class="ff-note">LevelDriven(D·B)은 레벨 제어(LIC)가 SP를 결정. 권장값은 참고 — 인가는 운전원.</div>
|
||||
</div>
|
||||
${thermo}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Hc900Crawler.Core.Application.Feedforward;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hc900Crawler.Infrastructure.Control;
|
||||
|
||||
@@ -36,6 +37,9 @@ public sealed class ColumnState
|
||||
// WP5 1단계: 2-point front baseline (C pivot 기준 상/하 차온의 느린 baseline)
|
||||
public FirstOrderLag UpperFrontBase { get; } = new(); // ΔT(C−D) 상부
|
||||
public FirstOrderLag LowerFrontBase { get; } = new(); // ΔT(C−B) 하부
|
||||
// 작업플랜-민감단온도 모듈1: T_C FB 경량 트림 상태
|
||||
public double TcFbDwellTimerSec { get; set; } // T_C FB dwell 타이머(20분 정착 후 작동)
|
||||
public double TcFbIntegral { get; set; } // T_C FB 적분항(편차 누적, gain 적용 전)
|
||||
// WO-6 전환류 상태기계
|
||||
public ColumnMode Mode { get; set; } = ColumnMode.Normal;
|
||||
public double ImbalanceTimerSec { get; set; }
|
||||
@@ -55,6 +59,15 @@ public sealed class ColumnState
|
||||
|
||||
public sealed class FeedforwardEngine
|
||||
{
|
||||
private readonly SteamAdvisor _steam;
|
||||
private readonly ILogger<FeedforwardEngine> _logger;
|
||||
|
||||
public FeedforwardEngine(SteamAdvisor steam, ILogger<FeedforwardEngine> logger)
|
||||
{
|
||||
_steam = steam;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public AdvisoryResult Tick(ColumnConfig cfg, PvSnapshot pv, ColumnState st, DateTime now)
|
||||
{
|
||||
var ts = cfg.ScanSec;
|
||||
@@ -145,13 +158,47 @@ public sealed class FeedforwardEngine
|
||||
if (tp.State is "온도역전" or "프로파일붕괴") frontTrim = null;
|
||||
var (mode, modeReason, feedRecSp) = ApplyRecovery(
|
||||
cfg, pv, st, ts, ff, vLossMa, frontState, tp.State, transient, ref outs); // WO-6 전환류 복귀
|
||||
|
||||
// ── 작업플랜-민감단온도 모듈1: T_C 유지 Steam OP 제안 ─────────────
|
||||
double? steamRecOp = null; string? steamConf = null; double? tcDev = null;
|
||||
if (mode == ColumnMode.Normal && !transient && Num.IsFinite(cfg.TcReturnTcTarget))
|
||||
{
|
||||
var tcTemp = ResolveTcTemp(cfg, pv);
|
||||
if (Num.IsFinite(tcTemp))
|
||||
{
|
||||
double feed = ff;
|
||||
double product = TryStreamPv(pv, cfg.ProductKey ?? "P", out var pvP) ? pvP : 0.0;
|
||||
var sa = _steam.Predict(feed, product, cfg.TcReturnTcTarget);
|
||||
steamConf = sa.Confidence;
|
||||
double error = tcTemp - cfg.TcReturnTcTarget;
|
||||
tcDev = error;
|
||||
const double deadband = 0.1, gain = -0.6, dwellSec = 1200.0;
|
||||
if (Math.Abs(error) > deadband) st.TcFbDwellTimerSec += ts;
|
||||
else { st.TcFbDwellTimerSec = 0; st.TcFbIntegral = 0; }
|
||||
if (sa.RecOp is double baseOp)
|
||||
{
|
||||
if (st.TcFbDwellTimerSec >= dwellSec)
|
||||
{
|
||||
st.TcFbIntegral += error * ts / 3600.0;
|
||||
st.TcFbIntegral = Math.Clamp(st.TcFbIntegral, -3.0, 3.0);
|
||||
steamRecOp = Math.Round(Math.Clamp(baseOp + gain * error + 0.05 * st.TcFbIntegral, 0, 100), 1);
|
||||
}
|
||||
else steamRecOp = Math.Round(baseOp, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AdvisoryResult(cfg.Id, cfg.Name, now, cfg.Enabled,
|
||||
transient, treason, ff, outs, vloss, yield, mbState)
|
||||
{ Temps = temps, VLossMa = vLossMa, FrontPositionState = frontState, FrontTrimAdvice = frontTrim,
|
||||
Mode = mode, ModeReason = modeReason, FeedRecommendedSp = feedRecSp,
|
||||
TempProfileState = tp.State, InversionPair = tp.InversionPair, TempSpan = tp.Span, TempSpanRef = tp.SpanRef,
|
||||
UpperFrontState = f2.upState, LowerFrontState = f2.loState,
|
||||
UpperFrontMetric = f2.upMetric, LowerFrontMetric = f2.loMetric };
|
||||
UpperFrontMetric = f2.upMetric, LowerFrontMetric = f2.loMetric,
|
||||
SensitiveTrayTag = cfg.SensitiveTrayTag,
|
||||
TcReturnTcTarget = double.IsNaN(cfg.TcReturnTcTarget) ? null : cfg.TcReturnTcTarget,
|
||||
TcReturnTcBand = cfg.TcReturnTcBand,
|
||||
TempLowLimit = double.IsNaN(cfg.TempLowLimit) || cfg.TempLowLimit <= -1e8 ? null : cfg.TempLowLimit,
|
||||
SteamRecOp = steamRecOp, SteamConfidence = steamConf, TcDeviation = tcDev };
|
||||
}
|
||||
|
||||
// ── WO-2 P-2 + WP6: 압력보정온도(PCT) 모니터 — 국소 압력 프로파일 적용 ──
|
||||
@@ -539,6 +586,17 @@ public sealed class FeedforwardEngine
|
||||
return cfg.FeedRecoverySp;
|
||||
}
|
||||
|
||||
private static double ResolveTcTemp(ColumnConfig cfg, PvSnapshot pv)
|
||||
{
|
||||
if (pv.Temps is null || cfg.SensitiveTrayTag is null) return double.NaN;
|
||||
var key = cfg.SensitiveTrayTag.EndsWith(".pv", StringComparison.OrdinalIgnoreCase)
|
||||
? cfg.SensitiveTrayTag : cfg.SensitiveTrayTag + ".pv";
|
||||
foreach (var t in pv.Temps)
|
||||
if (t.Tag.Equals(key, StringComparison.OrdinalIgnoreCase) && t.Good && Num.IsFinite(t.Value))
|
||||
return t.Value;
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
private static (double? rec, string note) ComputeStream(
|
||||
StreamConfig s, double ff, double dF, double ts, StreamState stt)
|
||||
{
|
||||
|
||||