Files
ExperionCrawler/docs/측류추출식-통합유량설정공식-구현코딩-PhaseII-분석엔진+전환류복귀.md

439 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 측류추출 통합유량 — Phase II 분석엔진 + 전환류 복귀 모드 구현 작업지시서
> **성격**: PhaseI(advisory 엔진)·PhaseII-UI(Tab 18)·PhaseIII(auto-write) 문서의 **§6 잔여 보정항목(P-1~P-5)** 과
> **신규 안전기능(전환류 평형복귀 모드)** 의 턴키 구현 작업지시서. 다른 LLM이 코드베이스 추가 탐색 없이 구현하도록
> **검증된 현재 코드 기준선** 위에서 작성한다.
>
> **불변식(PhaseI 계승)**: P-1~P-5 및 전환류 모드의 **권장(advisory) 계산까지는 제어 레지스터 쓰기 0건**.
> 실제 SP 쓰기(전환류 자동 실행 포함)는 **전부 PhaseIII(WriteGuard)** 경유. 본 문서는 "권장값/모드 산출 + 표시"까지가 범위.
>
> **작업 순서(영향도·의존성 반영)**: §A 문서감리 선반영 → **WO-1(P-5)** → **WO-2(P-2)** → **WO-3(P-1)** →
> **WO-4(P-4)** → **WO-5(P-3)** → **WO-6(전환류 복귀)** → §C 통합검증. P-7은 기존 PhaseIII 문서(§D 정정 메모 참조).
---
## §A. 선행 — 기존 문서·코드 감리 결과 (먼저 반영할 것) ★
본 작업 착수 전 아래 문서 드리프트를 인지하고 **§B 현재 코드 기준선을 정본**으로 삼는다. (기존 PhaseI/II/III 문서를 글자대로 따르면 깨지는 지점들.)
| # | 등급 | 위치 | 문제 | 조치 |
|:-:|:----:|:-----|:-----|:-----|
| A1 | **HIGH** | PhaseI §5.4 DDL·§5.8 seed·§2 모델, PhaseII §2.2 | `ff_stream_config.level_tag`(TEXT) 컬럼이 **실제 스키마·`StreamConfig.LevelTag`·`StreamAdvisory.LevelTag`·ConfigStore SELECT(인덱스 14)·SaveColumn INSERT·Controller `levelTag`** 에 전부 존재하나 **그 문서들엔 누락** | 본 문서는 **현재 코드 기준선(§B)** 을 정본으로 삼는다. 기존 DDL 문서를 그대로 따르지 말 것 |
| A2 | **WO-6 선결** | `src/Web/Program.cs:124~128` | (중복 버그 아님 — 현재 `AddHostedService<FeedforwardSupervisor>()` **단일 등록**으로 정상.) 단, WO-6 복구 컨트롤러가 `FeedforwardSupervisor`에 주입 접근(ColumnState ARM)하려면 **singleton 노출 필요** | `AddHostedService<FeedforwardSupervisor>()`(128) → **`AddSingleton<FeedforwardSupervisor>()` + `AddHostedService(sp=>sp.GetRequiredService<FeedforwardSupervisor>())`** 2줄로 교체(단일 인스턴스를 hosted+injectable로). **WO-6 착수 시에만** 변경, 그전엔 현 상태 유지 |
| A3 | **MED** | PhaseII §2.3 본문 vs §8 | §2.3 본문은 `IKbAuthService`/`IsAdminAsync`로 config CRUD를 막는 코드를 보여주나, §8과 **실제 컨트롤러는 인증 제거**(config CRUD 공개) | 본 문서는 **인증 없는 현재 컨트롤러(§B)** 기준. auth 재도입은 PhaseIII(쓰기 시)에서만 |
| A4 | LOW | PhaseI §2 | `StreamAdvisory` 레코드에 `LevelTag` 없음, enum에 `[JsonStringEnumConverter]` 표기 없음 — 실제는 둘 다 있음 | §B 기준선 사용 |
| A5 | LOW | PhaseI §6 P-2 | "TempCorrection(已구현)" — `TempCorrection.PressureCompensated`는 존재하나 **엔진에서 호출되지 않는 죽은 코드** | WO-2에서 배선 |
| A6 | LOW | PhaseIII §6 | 변경대상 `src/Infrastructure/OpcUa/OpcUaClientService.cs`**그 이름의 파일이 없음**. 단, **`src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs`(namespace `...Infrastructure.Control`)가 이미 존재**(쓰기 클라이언트 — 가드 없음). NodeId `ns=3;s="{tag}.sp"` 규칙 미검증 | §D 참조 — PhaseIII는 **기존 `ExperionOpcWriteClient` 재사용/확장**(신규 파일 X). NodeId는 서버 브라우즈로 확정 |
> **advisory 쓰기 불변식의 정확한 범위**: 코드베이스 전체엔 범용 `ExperionOpcWriteClient`(다른 기능용)가 **존재**한다.
> 불변식은 **"FF advisory 경로가 그 쓰기 클라이언트를 호출하지 않는다"** 는 의미.
> 검증 grep은 **FF 파일에 한정**: `grep -rE "ExperionOpcWriteClient|Write.*Async" src/Infrastructure/Control src/Web/Controllers/FeedforwardController.cs` → 0건(현재 유지됨).
---
## §B. 검증된 현재 코드 기준선 (정본 — 2026-05-31 실독)
다른 LLM은 아래를 **사실로 간주**하고, 기존 PhaseI/II/III 문서와 충돌 시 **본 §B를 우선**한다.
### B.1 모델 (`src/Core/Application/Feedforward/FeedforwardModels.cs`)
- `enum StreamRole { Commanded, LevelDriven, Monitor }` · `enum Confidence { A, B, C }` — 둘 다 `[JsonConverter(typeof(JsonStringEnumConverter))]`.
- `StreamConfig`: Key, FlowTag, Role, **LevelTag(string?)**, TargetCoeff, ThetaUpSec, ThetaDnSec, TauSec, SpMin, SpMax, RateUpPerMin, RateDnPerMin, RefluxFromProduct, Grade.
- `ColumnConfig`: Id, Name, Enabled, AdvisoryOnly(=true 강제), FeedTag, PressureTag, LevelTags, ScanSec, FeedFilterTauSec, FeedMoveThresholdPerMin, PressFilterTauSec, PressureBand, SettleSec, StaleSec, ProductKey, Streams.
- `TagSample(Tag, Value, Good, Timestamp)`.
- `PvSnapshot(Feed, Pressure?, Levels[], Streams{key→TagSample})`.
- `StreamAdvisory(Key, FlowTag, Role, Pv, RecommendedSp?, Gap?, Trend, Valid, Grade, **LevelTag?**, Note)`.
- `AdvisoryResult(ColumnId, ColumnName, ComputedAt, Enabled, Transient, TransientReason, FeedFiltered, Streams[], VLoss?, Yield?, MassBalanceState)`.
### B.2 연산블록 (`src/Infrastructure/Control/ComputationBlocks.cs`)
`Num`(Clamp/IsFinite), `FirstOrderLag`(Seed/Step), `MovingAverage(windowSamples)`(Push), `DeadTimeBuffer`(Through — 증가전용 링·비대칭 θ 보존), `RateLimiter`(Seed/Step 비대칭), `Derivative`(Update), `TempCorrection.PressureCompensated`(**미배선 죽은 코드**).
### B.3 엔진 (`src/Infrastructure/Control/FeedforwardEngine.cs`)
- `StreamState{ Dead, Lag, Rate, LastRec }`, `ColumnState{ FeedFilter, PressFilter, FeedDeriv, SettleTimerSec, Initialized, Streams{key→StreamState} }`.
- `FeedforwardEngine.Tick(ColumnConfig cfg, PvSnapshot pv, ColumnState st, DateTime now) → AdvisoryResult`**순수함수(I/O 없음)**.
- 흐름: FEED 품질게이트→Hold / FEED 필터(EMA) / 시드 / 과도·압력 게이트(transient) / 스트림 2-pass(commanded→reflux) / 물질수지(transient 아닐 때 `vloss=ff-(D+P+B)`, `yield=100*P/ff`).
- 역할별: Commanded=비대칭θ DeadTime→Lag→K→RateLimit / LevelDriven=`K*ff`(즉시) / Monitor=null.
### B.4 수급/저장/컨트롤러/DI
- `FeedforwardSupervisor`(BackgroundService): 스코프마다 `IFeedforwardConfigStore.LoadAllAsync` + `IExperionDbService.GetRealtimeRecordsByTagNamesAsync``PvSnapshot` 구성 → `Tick``IFeedforwardAdvisoryStore.Set`. 쓰기 없음. 컬럼별 `ColumnState` 단일 루프 소유(락 없음).
- `IFeedforwardConfigStore{ LoadAllAsync, SaveColumnAsync, DeleteColumnAsync }`, `IFeedforwardAdvisoryStore{ Set, Get, GetAll }`.
- `FeedforwardConfigStore`: ADO(EF 커넥션). LoadAll은 컬럼+스트림 2쿼리(스트림 SELECT에 `level_tag` 인덱스 14 포함). Save/Delete는 **파라미터화 `P()` 헬퍼 + 트랜잭션**.
- `FeedforwardController` (`api/ff`): `GET/POST/DELETE config`(**인증 없음**), `GET advisory`·`advisory/{id}`(공개). `MapColumn`/`MapConfig`로 camelCase 명시.
- DI(Program.cs:124~128): Engine=Singleton, AdvisoryStore=Singleton, ConfigStore=Scoped, Supervisor=**단일 AddHostedService**(중복 없음). WO-6에서 singleton+hosted로 교체(A2).
- DDL(ExperionDbContext.InitializeAsync, line ~1066~1103): `ff_column_config`, `ff_stream_config`(grade, **level_tag** 포함)을 **하나의 `ExecuteSqlRawAsync`에 여러 문장(CREATE;CREATE;ALTER)** 으로 멱등 생성(Npgsql는 다문장 허용 — PhaseI의 "한 호출 한 문장" 주의는 현재 코드에선 무효). try/catch로 "[ExperionDb] 초기화 실패" 경고 후 `return false`.
### B.5 C-6111 태그 매핑 (브레인스토밍 확정)
| 키 | 태그 | 의미 | 역할(seed) | 전환류 시 |
|:--:|:-----|:-----|:----------|:----------|
| FEED | ficq-6101 | 원료 투입(물질수지 기준) | (입력·외란) | **차단(→0)** |
| R | ficq-6113 | 환류(reflux) | Commanded(RefluxFromProduct) | **최대(전량 환류)** |
| P | ficq-6118 | 측류 제품(PGMEA) | Commanded | **차단(→0)** |
| D | ficq-6114 | 탑정 경비물(저비점) 배출 | LevelDriven | **차단(→0)** |
| B | ficq-6116 | 탑저 중비물(고비점) 배출 | LevelDriven | **차단(→0)** |
| 보조 | tica-6111a(탑저/리보일러), pica-6111·pi-6111(진공 ~48.5torr), lica-6113/li-6111(레벨), ti-6111b/c/d(프로파일) | | |
> 출처: D/P/B 매핑 = `knowledge/PGMEA_측류추출운전방식_주의점.md §1` + PhaseI §5.8 seed. 태그 계측현황 = `docs/보조운전-브레인스토밍.md §10.2`(ti-6111a.pv=0 고장의심 주의).
> **운전 정석(`PGMEA_측류추출운전방식_주의점.md §4.2·§4.3`)**: 측류 조성이 목표 이탈/외란 시 → ① **측류를 먼저 줄이거나 일시 중단**(오염 제품 방지) → ② **환류비를 높여 탑 내부 재안정화** → ③ 회복되면 측류 재개. "탑을 안정시킨 후 뽑는다"가 원칙. **전환류 모드(WO-6)는 이 §4.3 정석의 극단(드로우 전면 중단·전량 환류)을 상태기계로 형식화한 것.**
---
## §0. 모델 공통 확장 (모든 WO 선행) — `FeedforwardModels.cs`
WO들이 공유하는 필드를 **한 번에** 추가한다(레코드라 `with` 호환). **camelCase 직렬화는 컨트롤러 Map에서 명시** (PhaseI 규칙).
```csharp
// enum 추가
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ColumnMode { Normal, Recovering, Returning } // WO-6 전환류 상태기계
// StreamConfig 추가 필드 (DDL ff_stream_config 동반)
// P-1(θ제안)·전환류 reflux 식별용
public bool IsReflux { get; init; } // 전환류 시 "전량 환류" 대상. (없으면 RefluxFromProduct로 대체식별)
public double RecoverySp { get; init; } = double.NaN; // 전환류 중 이 스트림 권장SP. NaN이면 규칙기본(draw=0, reflux=SpMax)
// ColumnConfig 추가 필드 (DDL ff_column_config 동반)
// P-2(PCT/차온)
public IReadOnlyList<string> TempTags { get; init; } = Array.Empty<string>(); // 프로파일 온도 base tag (상→하 순서)
public string? SensitiveTrayTag { get; init; } // 민감트레이(프론트 지표). null이면 ΔT(상-하)로 대체
public double DTdP { get; init; } = 0.0; // dT/dP [°C/압력단위]. 0이면 PCT 미적용(생온도)
public double PRef { get; init; } = double.NaN; // 압력 기준점. NaN이면 첫 정상압력 시드
// P-1(θ 자동튜닝)
public string? SteamOpTag { get; init; } // TICA-6111A.OP(스팀) — 부분상관 2번째 입력(폐루프 오염 회피)
public bool ThetaAutoTune { get; init; } // θ 식별 가동 여부(제안만, 자동반영 X)
// P-4(느린 바이어스)
public double BiasMaWindowSec { get; init; } = 6*3600; // K_obs/k_V 장기 MA 창(기본 6h)
// WO-6(전환류 복귀)
public bool RecoveryEnabled { get; init; } // 전환류 권장 기능 on/off
public bool RecoveryAutoArm { get; init; } // true=자동권장, false=운전원 1클릭 무장 후에만
public double ImbalanceTriggerFrac { get; init; } = 0.10; // |V_loss_MA|/F 지속 초과 시 트리거(기본 10%)
public double ImbalanceTriggerSec { get; init; } = 600; // 지속 시간(기본 10분)
public double RecoverySettleSec { get; init; } = 1800; // 전환류 평형 dwell(기본 30분)
public double ReturnRampSec { get; init; } = 600; // 복귀 시 draw/feed 램프(기본 10분)
public double FeedRecoverySp { get; init; } = 0.0; // 전환류 중 FEED 권장값(기본 0=차단)
public string? DeltaPTag { get; init; } // 탑 차압(ΔP) — 플러딩/비산 트리거(주의점§3 4순위). null=미사용
public double DeltaPFloodLimit { get; init; } = double.MaxValue; // ΔP 상한(초과 지속 시 트리거)
// StreamAdvisory 추가 필드
public string? GradeReason { get; init; } // P-5 강등 사유
public double? ThetaSuggestUpSec { get; init; } // P-1 제안 θ↑ (null=신뢰부족)
public double? ThetaSuggestDnSec { get; init; } // P-1 제안 θ↓
public double? ThetaSuggestConf { get; init; } // P-1 상관 신뢰 0~1
public double? KObsSuggest { get; init; } // P-4 관측 K 장기추세 제안
// AdvisoryResult 추가 필드
public ColumnMode Mode { get; init; } = ColumnMode.Normal; // WO-6
public string? ModeReason { get; init; }
public double? VLossMa { get; init; } // P-4/WO-6 장기 MA V_loss
public IReadOnlyList<TempPoint>? Temps { get; init; } // P-2 PCT/차온 모니터
public string? FrontPositionState { get; init; } // P-3
public string? FrontTrimAdvice { get; init; } // P-3
public sealed record TempPoint(string Tag, double Raw, double Pct, bool Good);
```
> **레코드 확장 주의**: `StreamAdvisory`·`AdvisoryResult`는 **위치 인자(positional) record** 다. 새 필드는 **positional 파라미터로 추가하면 기존 `new StreamAdvisory(...)` 호출이 전부 깨진다.** → **새 필드는 위와 같이 `{ get; init; }` 본문 프로퍼티로 추가**(positional 생성자 불변)하고, 생성부에서 `with { ... }` 또는 object initializer로 채운다. 엔진의 기존 `new StreamAdvisory(...)`/`new AdvisoryResult(...)` 호출은 그대로 두고 뒤에 `with { GradeReason = ..., Mode = ... }`를 붙인다.
**DDL 델타** (ExperionDbContext.InitializeAsync, line ~1102 기존 ff 블록 끝 — 같은 `ExecuteSqlRawAsync` 다문장 블록에 멱등 ALTER 추가; Npgsql 다문장 허용. 위치는 `ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS level_tag TEXT;` 바로 뒤):
```sql
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS is_reflux BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS recovery_sp DOUBLE PRECISION; -- NULL=규칙기본
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS temp_tags TEXT; -- 콤마구분
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS sensitive_tray_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS dtdp DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS p_ref DOUBLE PRECISION; -- NULL=시드
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS steam_op_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS theta_auto_tune BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS bias_ma_window_sec DOUBLE PRECISION NOT NULL DEFAULT 21600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_auto_arm BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_frac DOUBLE PRECISION NOT NULL DEFAULT 0.10;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_sec DOUBLE PRECISION NOT NULL DEFAULT 600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_settle_sec DOUBLE PRECISION NOT NULL DEFAULT 1800;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS return_ramp_sec DOUBLE PRECISION NOT NULL DEFAULT 600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_recovery_sp DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_flood_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9;
```
> **ConfigStore 동반 수정**: `LoadAllAsync`의 두 SELECT에 신규 컬럼 추가(읽기 인덱스 시프트 주의 — 새 컬럼은 **항상 SELECT 끝에 append**하고 새 인덱스로 읽을 것), `SaveColumnAsync`의 INSERT/UPDATE에 신규 파라미터 추가, `MapConfig`에 camelCase 필드 추가. **인덱스 시프트가 PhaseI 진단의 단골 버그**이므로 SELECT 컬럼 순서와 `rd.GetXxx(n)` 번호를 1:1 대조 검증.
---
## WO-1 — P-5 confidence 자동강등 (1순위, 노력 小)
**목적**: config의 정적 `Grade`(A/B/C)를 **실시간 입력 건전성으로 강등**해, 운전원이 "지금 이 권장값을 믿을지"를 색으로 안다. PhaseIII auto-write의 **안전 게이트 전제**(Grade A만 쓰기).
**근거**: spec §14.3(보정 3등급), §14.5(신뢰도 플래그). PhaseII §6 훅("`StreamAdvisory.Grade` ← P-5 연결점").
**설계**: config `Grade`**상한(best-case)**. 엔진이 아래 사유로 한 단계씩 강등하고 `GradeReason`에 기록.
| 강등 사유 | 적용 |
|:----------|:-----|
| PV stale/BAD(신선도 초과) | 해당 스트림 → 최소 B, 연속 시 C |
| 과도(transient) 중 | → 한 단계 강등(정착 전 신뢰 낮음) |
| 압력 불안정(pUnstable) | 컬럼 전체 → 한 단계 강등 |
| analyzer/온도지표 부재인데 등급이 C 항목 의존(P-3 연계) | C 유지 |
| 물질수지 불일치(`mbState`≠"정상") | commanded 스트림 → 한 단계 강등 |
**구현 (`FeedforwardEngine.cs`)** — 순수함수 유지. 헬퍼 추가:
```csharp
private static (Confidence g, string? why) Downgrade(Confidence baseG, params (bool hit,string why)[] rules)
{
int lvl = (int)baseG; // A=0,B=1,C=2
string? why = null;
foreach (var (hit, w) in rules) if (hit) { lvl = Math.Min(2, lvl + 1); why = why is null ? w : why + "; " + w; }
return ((Confidence)lvl, why);
}
```
`BuildAdvisory`에서 스트림별 적용(`pv.Streams[key].Good` 신선도, `transient`, mbState 불일치 플래그를 인자로 받게 시그니처 확장), 결과를 `with { Grade = g, GradeReason = why }`. 컬럼 공통 사유(pUnstable)는 Tick에서 일괄 한 단계 추가 강등.
**UI(ff.js, 기존 P-6 자산)**: 이미 `.ff-grade-A/B/C` 색 클래스 존재 → `s.gradeReason`을 셀 `title`(툴팁)로 노출만 추가.
**테스트(xUnit, PhaseI §5.7 프로젝트)**:
- 신선 PV + 정상 → config Grade 그대로.
- stale PV → ≥B. transient → 한 단계. stale+transient+mb불일치 → C(바닥).
- `Downgrade`는 A를 넘어 C에서 더 내려가지 않음(클램프).
**검증**: 빌드 0/0, `grep`로 쓰기 0건 유지, advisory 응답에 `gradeReason` 등장, 한 스트림 강제 stale 시 해당 카드만 강등.
---
## WO-2 — P-2 PCT/차온 모니터 (2순위, 반쯤 완성)
**목적**: 죽은 코드 `TempCorrection.PressureCompensated`**엔진에 배선** + `DiffTemp` 추가 → 진공노이즈 제거된 PCT·차온을 모니터로 산출. **P-3(프론트 위치)·P-5(C등급 근거)의 입력**.
**근거**: spec §13.3(PCT/ΔT), §13.6(블록), §14.1(dT/dP≈0.5°C/torr — 진공 ±2torr가 구배 절반 → PCT 필수).
**신규 블록 (`ComputationBlocks.cs`)**:
```csharp
/// <summary>차온/이중차온 — 공통모드(압력·계절) 상쇄, 프론트 이동만 부각. §13.3</summary>
public static class DiffTemp
{
public static double Delta(double tHi, double tLo) => tHi - tLo; // 두 트레이 차온
public static double Double(double tA, double tB, double tC) => (tA - tB) - (tB - tC); // 이중차온(곡률)
}
```
> `TempCorrection.PressureCompensated`는 그대로 사용(추가 코드 없음). PCT = `T_meas dTdP·(P P_ref)`.
**엔진 배선**:
- `ColumnState``FirstOrderLag PRefSeed`(또는 단순 `double _pRef`) 추가 — `cfg.PRef`가 NaN이면 첫 정상 압력으로 시드.
- `BuildSnapshotAsync`(Supervisor)에 `cfg.TempTags` PV 읽기를 추가(이미 feed/pressure/levels/streams 읽는 패턴 재사용). `PvSnapshot``IReadOnlyList<TagSample> Temps`를 추가(positional이므로 **새 record 필드는 init 프로퍼티로** 추가하거나 `PvSnapshot`를 확장 — 본 문서는 `PvSnapshot``Temps` init 프로퍼티 추가 권장).
- Tick에서 각 온도에 PCT 계산 → `AdvisoryResult.Temps`(`TempPoint`)로 저장. `dTdP==0`이면 PCT=raw(생온도, 모니터만).
- **advisory-only**: Temps는 표시·P-3 입력일 뿐 권장SP에 영향 없음(이번 WO 한정).
**ConfigStore/Controller/DDL**: §0의 `temp_tags`·`dtdp`·`p_ref`·`sensitive_tray_tag` 반영(load/save/map).
**UI(ff.js)**: 카드 하단에 온도 미니행(태그·raw·PCT) 1줄. 없으면 생략.
**테스트**: `TempCorrection`(P 상승 시 PCT가 raw보다 낮아짐, dTdP=0이면 PCT=raw), `DiffTemp.Delta/Double` 산술, `PRef` NaN 시드 경로.
**검증**: dtdp>0 컬럼에서 진공 흔들 때 raw는 출렁이나 PCT는 평탄(공통모드 상쇄) 확인.
---
## WO-3 — P-1 θ 자동튜닝 (3순위, 노력 大·통계)
**목적**: seed θ/τ가 전부 placeholder(PhaseI §5.8 경고)인 문제를 해소. **정상 운전 중 자연외란**으로 θ를 **passive 식별**해 **제안만**(자동반영 금지·운전원 승인 시 config 반영).
**근거**: spec §13.4(교차상관 θ, 스팀 부분상관으로 폐루프 오염 회피), §13.7(θ는 신뢰도 등급 붙은 추정치).
> **현실 경고(spec §13.2)**: 단일점 생온도 SNR 낮음 → **WO-2의 PCT/ΔT를 입력으로** 쓰고, **스팀 OP(`SteamOpTag`)를 2번째 입력으로 부분상관**해 TICA 폐루프 동특성을 θ로 오귀속하지 않게 한다. 외란 부족 시 **신뢰도 낮음 → 제안 억제(null)**.
**신규 블록 (`ComputationBlocks.cs` 또는 신규 `CrossCorrLagEstimator.cs`)** — 계약(시그니처) 고정:
```csharp
/// <summary>
/// Passive 전달지연 식별. ΔF(피드 변화)와 ΔS_i(=ΔPCT/Δflow) 의 교차상관 최대 지연 = θ.
/// 스팀 ΔS_steam 을 2번째 입력으로 부분상관(partial corr)해 폐루프 오염 제거(§13.4).
/// 사전백색화(pre-whitening=1차차분) 적용. I/O 없음, 컬럼 루프 단일 소유(락 불필요).
/// </summary>
public sealed class CrossCorrLagEstimator
{
public CrossCorrLagEstimator(int maxLagSamples, int historySamples, double minSignalStd);
/// 매 틱 호출. 반환: 충분한 외란 누적 후에만 (θup,θdn,conf), 아니면 null.
public (double thetaUpSec, double thetaDnSec, double conf)? Push(
double dFeed, double dResponse, double dSteam, double tsSec);
}
```
**알고리즘(구현 지침)**:
1. 입력은 **1차차분(Δ)** 받음(미분=사전백색화). 링버퍼(historySamples)에 `dFeed`,`dResponse`,`dSteam` 누적.
2. 외란 검정: `std(dFeed) < minSignalStd` → 신뢰 0, null 반환(억제).
3. **부분상관**: `dResponse`에서 `dSteam` 선형회귀 성분 제거(잔차 `r = dResponse β·dSteam`, β=cov/var). 이후 `ρ(τ)=corr(dFeed[t], r[t+τ])` for τ∈[0,maxLag].
4. `θ = argmax_τ ρ(τ)·ts`. `conf = max ρ`(0~1, 음수면 0).
5. 상승/하강 비대칭: `dFeed>0` 표본만으로 θup, `dFeed<0` 표본만으로 θdn 별도 추정(표본 부족 시 공통값).
6. `conf < 0.3`면 제안 억제(null).
**배선**: `StreamState`에 컬럼당 1개 estimator(또는 commanded 스트림별). Tick에서 `cfg.ThetaAutoTune && WO-2 PCT 가용` 일 때만 `Push``StreamAdvisory.ThetaSuggestUpSec/DnSec/Conf` 채움. **config θ는 변경하지 않음**(제안 전용).
**UI**: 카드에 "θ 제안 ↑NNs ↓NNs (conf 0.x)" 보조행. 운전원이 설정 에디터에서 수동 반영.
**테스트**: 합성 신호(알려진 θ로 지연된 응답 + 노이즈)에 대해 추정 θ가 ±1 샘플 내. 외란 std 미달 시 null. 스팀 상관 주입 시 부분상관이 제거하는지(스팀만 상관된 가짜 지연은 conf 낮게).
**검증**: 라이브에서 ThetaAutoTune=true 컬럼이 외란 충분 시에만 제안 노출, config 무변경(쓰기 0건) 확인.
> **현실성**: 데모 시스템 온도는 인위 생성(spec §13.7) → 실플랜트 전 **검증 보류 가능**. 본 WO는 **인터페이스·블록·테스트까지** 턴키로 두되, 가동 스위치(`ThetaAutoTune`)는 기본 false.
---
## WO-4 — P-4 느린 바이어스 적응 (4순위)
**목적**: 계절 CW 스윙 등 **크지만 느린 외란**(spec §14.4)을 정밀모델 대신 **장기 MA로 K_obs·k_V 추세**를 내어 운전원에게 "계절 보정 제안". 자동 변경 아님.
**근거**: spec §14.4(느린 바이어스/운전원 트림), §14.3 B등급(V_loss는 장기 MA로만 의미).
**설계**:
- `ColumnState``MovingAverage`(창=`BiasMaWindowSec/ScanSec` 샘플) 2개: `VLossMa`, 그리고 commanded 스트림별 `KObsMa`(=PV/FeedFiltered 의 MA).
- **정상상태에서만 갱신**(transient·BAD 제외) — 과도 표본 오염 방지.
- 산출: `AdvisoryResult.VLossMa`, `StreamAdvisory.KObsSuggest`(= K_obs MA, config TargetCoeff와 비교해 드리프트 표시).
- **advisory-only**: 제안값일 뿐 엔진 K는 config 그대로.
**테스트**: 일정 비율 입력 스텝 후 MA가 천천히 수렴(창 길이만큼), 과도 표본은 MA에 안 들어감.
**검증**: 장기 가동 후 KObsSuggest가 config K 부근, 인위적 bias 주입 시 추세 이동.
> WO-4의 `VLossMa`는 **WO-6 전환류 트리거 입력**으로 재사용(순간 V_loss는 §5.3대로 신뢰불가 → MA로 판정).
---
## WO-5 — P-3 Sweet-Spot / 프론트 위치 지표 (5순위, P-2 의존)
**목적**: WO-2의 제품존 PCT/ΔT(민감트레이)를 **프론트 위치 프록시**로 삼아 드리프트 시 **환류/boilup 트림 권장**(advisory). analyzer 있으면 우선.
**근거**: spec §13.5(2층 구조: 빠른 에너지=피드포워드, 느린 조성=온도 피드백), §13.2 함정②(제품존 신호 약함 → 차온 필수), §14.3 C등급.
**신규 블록 (`FrontPositionIndicator.cs`)**:
```csharp
/// <summary>제품존 PCT/ΔT 의 기준대비 드리프트 → sweet-spot 건전성 + 트림 방향 권장. advisory.</summary>
public sealed class FrontPositionIndicator
{
public FrontPositionIndicator(double bandwidth, double refTau);
public (string state, string? trimAdvice, Confidence grade) Update(
double frontMetric, double tsSec); // frontMetric = 민감트레이 PCT 또는 제품존 ΔT
}
```
- 느린 기준(EMA refTau, 예 30~60min)에서 `frontMetric` 이탈량 산출.
- 밴드 내=「정상」 / 위로 드리프트=「프론트 상승 — 경비물 혼입 위험: 환류↑ 권장」(브레인스토밍 Q2/Q3 정석) / 아래=「프론트 하강 — boilup↑/환류↓ 권장」.
- 등급: 단일 생온도면 C(신호 약함), 차온/analyzer면 B 이상.
- **트림은 권장 문구만**(`AdvisoryResult.FrontPositionState`/`FrontTrimAdvice`) — SP 미변경.
**배선**: WO-2 Temps에서 `SensitiveTrayTag`(없으면 상-하 ΔT) 추출 → Indicator.Update → AdvisoryResult 필드.
**UI**: 카드 배너에 프론트 상태/트림 권장 표시(P-6 `ff-note` 자리 활용).
**테스트**: 기준 대비 상/하 드리프트에 대한 state·trim 분기, 밴드 내 「정상」, 등급 강등(생온도→C).
---
## WO-6 — 전환류(Total Reflux) 평형복귀 모드 ★ 신규
**요구(운전원)**: 컬럼 균형이 **심각하게 깨졌다고 판단되면**, **전환류 모드**로 전환 — **제품(P)·원료투입(F)·경비물(D)·중비물(B) 제거를 모두 차단**하고 **환류(R)를 전량 환류**로 두어 **평형 복귀**할 때까지 유지, 회복 후 정상 복귀.
**공정 근거**: 증류 정석의 *total reflux* 회복기동 = `knowledge/PGMEA_측류추출운전방식_주의점.md §4.3`("외란 시: 측류 먼저 중단 → 환류비↑ 재안정화 → 회복 후 재개")의 극단형. 외란으로 조성 프론트가 무너지면, 드로우·피드를 끊고 내부 환류만 순환시키면 단별 조성 프로파일이 **재평형**된다. 측류추출탑은 측류 조성이 상부 경비물·하부 중비물 **균형에 전적 의존**(동 §2)하므로 균형 붕괴 시 **측류 혼입(off-spec) 직전 회복 카드**.
### 6.1 아키텍처 결정 (불변식 준수)
- **Phase II 범위 = 권장·상태표시까지**. 전환류는 본질적으로 **쓰기 동작**(드로우/피드 SP=0, 환류=max)이므로, **실제 실행은 PhaseIII(WriteGuard) 경유**. 본 WO는 **모드 판정 상태기계 + 권장 SP 산출 + UI 경보/표시**까지(쓰기 0건).
- **트리거 권위 2모드**: `RecoveryAutoArm=false`(기본) → 엔진은 "전환류 권장(ARMED)"만 띄우고 **운전원 1클릭 확인** 후 Recovering 진입(PhaseIII에서 실제 쓰기). `=true` → 자동 권장 상태기계가 직접 Recovering 진입(여전히 표시/권장, 쓰기는 PhaseIII gating).
- **오발동 비용 큼**(피드 차단=생산중단) → 트리거는 **순간 V_loss 금지**(§5.3). 과도(transient) 중엔 트리거 금지.
- **다신호 트리거(canonical 근거: `PGMEA_측류추출운전방식_주의점.md §3`)** — "균형 심각 붕괴"는 단일 V_loss보다 아래 **지속 조건의 OR**로 판정(어느 하나라도 `ImbalanceTriggerSec` 지속 시 ARM):
- **① 물질수지**: `|VLossMa|/F > ImbalanceTriggerFrac` (WO-4 장기 MA).
- **② 프론트(감도트레이)**: WO-5 `FrontPositionState`=심각 드리프트(주의점 §3 1순위 = 감도트레이 온도. **가장 신뢰도 높은 조기신호**).
- **③ 플러딩/비산**: `DeltaPTag` 가용 시 `ΔP > DeltaPFloodLimit` (주의점 §3 4순위 — 비산이 제품 오염 직결).
- 신호별 가용성은 config로 결정(태그 없으면 해당 조건 비활성).
### 6.2 상태기계 (`ColumnMode`)
```
┌────────────────────────── Normal ──────────────────────────┐
│ |VLossMa|/F > ImbalanceTriggerFrac 지속> ImbalanceTriggerSec │
│ AND !transient │
│ AND (RecoveryAutoArm OR 운전원 ARM 확인) │
▼ │
Recovering(전환류) │
권장: F→FeedRecoverySp(0), P/D/B→0(또는 RecoverySp), R→SpMax(전량) │
dwell: 평형지표 안정(|VLossMa|/F < Frac·0.5 AND 프론트(WO-5) 정상) │
을 RecoverySettleSec 동안 연속 만족 │
▼ │
Returning(복귀 램프) │
ReturnRampSec 동안 F·draw를 정상 권장값으로 RateLimiter 램프 복원 │
완료 → Normal ──────────────────────────────────────────────────┘
(어느 상태든 운전원 수동 Normal 복귀 가능 / FEED BAD 지속 시 Recovering 유지)
```
### 6.3 엔진 구현 (`FeedforwardEngine.cs` + `ColumnState`)
- `ColumnState`에 추가: `ColumnMode Mode`, `double ImbalanceTimerSec`, `double RecoverySettleTimerSec`, `double ReturnTimerSec`, `bool OperatorArmed`(컨트롤러가 set).
- **순수성 유지**: 상태 전이는 Tick 내에서 `st`(가변 ColumnState)로 처리 — 기존 SettleTimer와 동일 패턴(I/O 없음).
- 절차(Tick 말미, advisory 산출 후):
1. `severe = ① || ② || ③` (위 다신호; 가용 신호만 평가). `① frac=|VLossMa|/ff > ImbalanceTriggerFrac`, `② WO-5 FrontPositionState 심각`, `③ ΔP>DeltaPFloodLimit`.
2. **Normal**: `!transient && severe``ImbalanceTimerSec += ts` else `=0`. `ImbalanceTimerSec ≥ ImbalanceTriggerSec` + (AutoArm || OperatorArmed) → `Mode=Recovering`, 타이머 리셋, OperatorArmed=false. (AutoArm=false면 ARM 없이는 "전환류 권장(ARMED 대기)" 표시만.)
3. **Recovering**: 스트림 권장값을 **오버라이드** — reflux(IsReflux||RefluxFromProduct)=`SpMax`, 그 외 commanded draw=`RecoverySp(NaN→0)`, FEED 권장=`FeedRecoverySp`. 평형조건(`frac < Frac*0.5 && 프론트 정상`) 연속 만족 시 `RecoverySettleTimerSec += ts`, 도달 시 `Mode=Returning`.
4. **Returning**: `ReturnTimerSec += ts`; 진행률 `α=min(1, ReturnTimerSec/ReturnRampSec)`로 draw/feed 권장을 0→정상값 보간(또는 RateLimiter가 자연 램프). `α>=1``Mode=Normal`.
5. `AdvisoryResult``with { Mode = st.Mode, ModeReason = ..., VLossMa = ... }`. Recovering/Returning에선 각 `StreamAdvisory.RecommendedSp`가 오버라이드값, `Note`에 "전환류 복귀" 표기. **Grade는 강등하지 않되 Valid=false(운전원 인가 필요)**.
- **FEED 권장 노출**: FEED는 스트림이 아니므로 `AdvisoryResult``FeedRecommendedSp`(double?) init 프로퍼티 추가 — Recovering 시 `FeedRecoverySp`, 그 외 null.
### 6.4 컨트롤러 (`FeedforwardController.cs`)
- `POST api/ff/recovery/{columnId}/arm` — 운전원 ARM(=Supervisor 통해 해당 ColumnState.OperatorArmed=true). **Supervisor를 singleton 주입**(A2 정정으로 가능)해 컬럼 상태 접근. 쓰기 아님(모드 판정용 플래그).
- `POST api/ff/recovery/{columnId}/cancel` — 수동 Normal 복귀.
- (PhaseIII에서) 실제 SP 쓰기는 별도 `apply` 엔드포인트가 WriteGuard 경유.
### 6.5 UI (ff.js / ff.css)
- 카드 헤더에 모드 뱃지: `Normal`(무표시) / `전환류 복귀중 ●`(주황) / `복귀 램프 ●`(파랑) / `전환류 권장(ARMED)`(점멸 경보 + [확인] 버튼 → arm 호출).
- Recovering/Returning 시 표에 "권장 SP" 열이 0/max 오버라이드로 표시, Valid=false라 흐리게(`ff-stale`)·"운전원 인가" 주석.
### 6.6 DDL/ConfigStore
§0의 recovery 컬럼들(`recovery_enabled`, `recovery_auto_arm`, `imbalance_trigger_frac`, `imbalance_trigger_sec`, `recovery_settle_sec`, `return_ramp_sec`, `feed_recovery_sp`) + stream `is_reflux`/`recovery_sp` 반영.
### 6.7 테스트 (xUnit)
- 합성 시퀀스: 정상 → VLossMa 임계 지속 초과(+!transient) → `Recovering` 진입. AutoArm=false면 OperatorArmed 없이는 **진입 안 함**.
- Recovering 권장값: reflux=SpMax, P/D/B=0, FeedRecommendedSp=FeedRecoverySp.
- 평형 회복(frac↓ 지속 RecoverySettleSec) → `Returning` → ReturnRampSec 경과 → `Normal`.
- transient 중엔 트리거 타이머 누적 안 됨.
- 수동 cancel 시 즉시 Normal.
- **쓰기 0건**(엔진/컨트롤러 grep) — 모드 판정·권장만.
### 6.8 안전 결정 (문서화)
| 항목 | 결정 |
|:-----|:-----|
| 실행 권한 | 권장·표시는 PhaseII, **실제 SP 쓰기는 PhaseIII WriteGuard** 경유(전환류는 대규모 조작이라 운전원 확인 강제 권장) |
| 오발동 방지 | 순간 V_loss 금지, `VLossMa`(장기 MA) 지속 초과 + `!transient` + (AutoArm||운전원 ARM) |
| 복귀 부드<EBB680>러움 | Returning에서 RateLimiter 램프 — bumpless |
| 트리거 보수성 | 기본 `RecoveryAutoArm=false`(운전원 1클릭). 자동무장은 신뢰 확보 후 |
| point of no return | 프론트(WO-5) "경비물 혼입 위험" 단계에서 **선제 ARM 권장**(정석) |
---
## §C. 통합 검증 (감독자 — diagnosis-checklist.md 8단계)
1. **빌드**: `dotnet build src/Web/ExperionCrawler.csproj` 경고0/에러0.
2. **쓰기 불변식**(FF 경로 한정): `grep -rE "ExperionOpcWriteClient|Write.*Async" src/Infrastructure/Control src/Web/Controllers/FeedforwardController.cs`**0건**(범용 `ExperionOpcWriteClient.cs`는 OpcUa 폴더라 미포함 — 정상). WO 전체 advisory.
3. **DI(A2)**: WO-6 적용 시 `FeedforwardSupervisor`가 singleton+hosted로 노출되어 **인스턴스 1개만 가동**(로그 틱 루프 1회) + 컨트롤러 주입 가능. WO-6 전이면 현 단일 AddHostedService 유지.
4. **인덱스 정합(§0)**: ConfigStore SELECT 컬럼 ↔ `rd.GetXxx(n)` 1:1, 저장→재로드 라운드트립 일치(신규 필드 포함).
5. **단위테스트**: WO-1~WO-6 케이스 + PhaseI 기존 4 모두 green.
6. **라이브**: Tab 18에서 등급 강등(WO-1)·PCT(WO-2)·θ제안(WO-3)·KObs추세(WO-4)·프론트 트림(WO-5)·전환류 모드 뱃지/ARM(WO-6) 표시. 폴링 누수 없음.
7. **전환류 시나리오**: 인위적 V_loss bias 주입 → VLossMa 임계 지속 → (ARM 후) Recovering 권장(R=max, F/P/D/B=0) → 회복 → Returning → Normal. 쓰기 0건.
---
## §D. P-7 (PhaseIII auto-write) — 기존 문서 정정 메모
PhaseIII 문서는 대체로 유효하나 착수 전 아래 정정:
- **D1**: PhaseIII §6이 지목한 `OpcUaClientService.cs`**그 이름으론 부재**. 그러나 **`src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs`(namespace `ExperionCrawler.Infrastructure.Control`, `Opc.Ua.Client` 사용)가 이미 존재** — 쓰기 래퍼를 **신규 작성하지 말고 이 기존 클라이언트를 재사용/확장**(가드는 WriteGuard로 상위에서). 기존 읽기 서비스(Realtime/History/Metadata 등)는 건드리지 말 것.
- **D2**: NodeId `ns=3;s="{tag}.sp"`**미검증 가정**`ExperionOpcWriteClient`의 실제 노드 지정 방식 + 서버 브라우즈로 **확인 후 확정**.
- **D3**: WriteGuard 게이트는 **WO-1의 동적 Grade**(정적 config Grade 아님) + `!Transient` + SafetyMaxDelta. WO-6 전환류는 별도 `apply` 경로로 **운전원 확인 강제**.
- **D4**: PhaseIII §1.2("B/C 금지")와 §4.3("스트림 단위 A만 대상")은 **per-stream 동적 Grade 기준**으로 통일.
- **D5**: auth 재도입(`IKbAuthService`)은 **쓰기 엔드포인트에만**(advisory/config 읽기는 공개 유지).
---
## §E. 턴키 요약
| WO | 항목 | 노력 | 신규 파일 | 변경 파일 | 가동 스위치(기본) |
|:--:|:-----|:----:|:----------|:----------|:------------------|
| A | 문서 드리프트 정정(§B 기준선화) | 小 | — | (WO-6 시 Program.cs DI 교체) | — |
| 0 | 모델·DDL 공통확장 | 中 | — | Models/ConfigStore/Context/Controller | — |
| 1 | P-5 등급 자동강등 | 小 | — | Engine, ff.js | 항상 |
| 2 | P-2 PCT/차온 | 小 | (DiffTemp 블록) | Blocks/Engine/Supervisor/Store/Ctrl | dtdp>0 시 |
| 3 | P-1 θ 자동튜닝 | 大 | CrossCorrLagEstimator | Engine/State/Supervisor | ThetaAutoTune=false |
| 4 | P-4 느린 바이어스 | 中 | — | Engine/State | 항상(제안) |
| 5 | P-3 프론트 위치 | 中 | FrontPositionIndicator | Engine/State | 온도 가용 시 |
| 6 | 전환류 복귀 | 大 | — | Engine/State/Ctrl/ff.js/css | RecoveryEnabled=false, AutoArm=false |
| D | P-7 정정 | — | (PhaseIII) | — | — |
**구현 순서**: A(정정) → 0(공통확장·DDL·ConfigStore 인덱스검증) → 1 → 2 → 3 → 4 → 5 → 6 → C(검증). 각 WO는 **빌드+해당 테스트 green + 쓰기 0건 grep** 후 다음 진행.
**불변식 재확인**: 본 문서 전체 범위에서 **제어 레지스터 쓰기 0건**. 전환류 포함 모든 실제 SP 쓰기는 PhaseIII.
</content>
</invoke>
<result>
File created successfully: /home/windpacer/projects/ExperionCrawler/docs/측류추출식-통합유량설정공식-구현코딩-PhaseII-분석엔진+전환류복귀.md