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

38 KiB
Raw Permalink Blame History

측류추출 통합유량 — 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.GetRealtimeRecordsByTagNamesAsyncPvSnapshot 구성 → TickIFeedforwardAdvisoryStore.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 10661103): 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 규칙).

// 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; 바로 뒤):

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) — 순수함수 유지. 헬퍼 추가:

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):

/// <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).

엔진 배선:

  • ColumnStateFirstOrderLag PRefSeed(또는 단순 double _pRef) 추가 — cfg.PRef가 NaN이면 첫 정상 압력으로 시드.
  • BuildSnapshotAsync(Supervisor)에 cfg.TempTags PV 읽기를 추가(이미 feed/pressure/levels/streams 읽는 패턴 재사용). PvSnapshotIReadOnlyList<TagSample> Temps를 추가(positional이므로 새 record 필드는 init 프로퍼티로 추가하거나 PvSnapshot를 확장 — 본 문서는 PvSnapshotTemps 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) — 계약(시그니처) 고정:

/// <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 가용 일 때만 PushStreamAdvisory.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로만 의미).

설계:

  • ColumnStateMovingAverage(창=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의 VLossMaWO-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):

/// <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 && severeImbalanceTimerSec += 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가 자연 램프). α>=1Mode=Normal.
    5. AdvisoryResultwith { Mode = st.Mode, ModeReason = ..., VLossMa = ... }. Recovering/Returning에선 각 StreamAdvisory.RecommendedSp가 오버라이드값, Note에 "전환류 복귀" 표기. Grade는 강등하지 않되 Valid=false(운전원 인가 필요).
  • FEED 권장 노출: FEED는 스트림이 아니므로 AdvisoryResultFeedRecommendedSp(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
복귀 부드<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.cs0건(범용 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.

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