# 작업지시서 — 안전 피드램프 Advisory (WP1·WP2·WP3) > 대상: 다른 LLM/개발자가 단독 실행 가능하도록 작성. 사전 맥락 없이 이 문서 + 참조 문서만으로 수행. > 작성: 2026-06-01. 컬럼: C-6111 PGMEA 측류추출 증류탑. > 설계 근거: `docs/안전피드램프-한계치-브레인스토밍.md` (§0~§9) — **반드시 먼저 통독**. --- ## 0. 배경 (요약) LevelDriven 드로우가 `recommendedSp=K×피드`를 무제한 상승 표시하는 문제에서 출발 → "피드를 평형 깨지 않고 얼마나·얼마나 빨리 올리나"를 계산하는 **advisory(쓰기 없음)** 로 전환. 본 작업은 그 첫 구현 + 검증 체계. 작업 패키지: - **WP0** Sim Override Layer (실행자가 입력값을 직접 조정해 자율 시뮬레이션; 제어 쓰기 아님) - **WP1** 검증 시나리오 매트릭스 (문서, 구현 spec 겸 합격기준) - **WP2** 현행 FF 엔진 프로빙 (시뮬레이션으로 baseline 거동 캡처) - **WP3** Feed Ramp Advisor 구현 (read-only 계산기 + 엔드포인트 + 단위테스트) 실행 순서: **WP1(spec) + WP0(테스트 하네스) → WP2(자율 프로빙) → WP3(구현) → WP3를 WP1로 검증**. --- ## 0.1 공통 제약 (전 WP 적용, 위반 금지) 1. **쓰기 절대 금지**: OPC UA 쓰기·DB 쓰기 없음. 현 시스템은 AdvisoryOnly 정책. WP3는 순수 계산 + 조회만. 2. **DEMO 시스템**: 모든 실시간 값은 운전원이 주입한 합성값(테스트 벤치). 로직/기능 검증엔 유효하나, 데모 값으로 실제 플랜트 τ/θ·플러딩 임계를 단정하지 말 것. (메모리 `project_demo_system_synthetic_data` 참조.) 3. **시뮬레이션 절차 (2가지 경로)**: - **(자율, 권장) Sim Override** — WP0에서 만드는 in-app 입력 오버라이드. 실행자(LLM)가 `POST /api/ff/sim/override`로 입력 태그값을 직접 세팅 → `GET /api/ff/ramp-advisor`/`advisory` 관찰. **운전원 없이 루프 자율 구동.** OPC/제어 SP 쓰기 아님(입력값 치환만). - **(실계측 경로) 운전원 주입** — 운전원이 합성 DCS 값을 맞춰줌. 실행자는 "태그=값" 요청 후 `GET /api/ff/advisory` 또는 `run_sql`(realtime_table)로 관찰. - 두 경로 결과는 일치해야 함(오버라이드는 동일 입력을 in-app으로 대체). 4. **JSON 직렬화**: 신규 DTO 반환 시 PascalCase 패스스루 주의 → `[JsonPropertyName("camelCase")]` 명시 (메모리 `reference_json_serializer_pascalcase`). 기존 컨트롤러는 익명객체로 수동 camelCase 처리하는 패턴도 있음 — `GetDashboard` 응답 빌더 참고. 5. **빌드/테스트 명령**: - 빌드: `dotnet build src/Web/ExperionCrawler.csproj` → 경고 0 / 에러 0 필수 - 테스트: `dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj` 6. **건드리지 말 것**: 기존 `FeedforwardEngine.Tick` 로직, `FeedforwardSupervisor.AutoWriteAsync`, config 스키마(ff_column_config/ff_stream_config). WP3는 **신규 파일 추가** 위주. --- ## 0.2 핵심 시스템 사실 (C-6111, column_id=1) ### 스트림 config (ff_stream_config) | key | flow_tag | role | K(target_coeff) | θ_up/dn(s) | τ(s) | sp_min/max | rate_up/dn(kg/hr·min) | level_tag | |---|---|---|---|---|---|---|---|---| | P | ficq-6118 | Commanded | 0.95 | 60/60 | 900 | 0/2000 | 30/60 | – | | R | ficq-6113 | Commanded | 0.80 | 0/0 | 0 | 0/2000 | 30/30 | – | | D | ficq-6114 | LevelDriven | 0.02 | – | – | 0/1000 | 0/0 | lica-6113 | | B | ficq-6116 | LevelDriven | 0.03 | – | – | 0/500 | 0/0 | li-6111 | ProductKey = P. 피드 = ficq-6101. (K합 P+D+B=1.00 물질수지 폐합, R=내부환류.) ### 컬럼 config (ff_column_config id=1) — 현재 dormant 항목 주의 - pressure_tag=**pica-6111만**, sensitive_tray_tag=**null**, delta_p_tag=**null**, steam_op_tag=**null**, p_ref=**null**, dtdp=**0.0**, temp_tags=`tica-6111a,ti-6111b,ti-6111c,ti-6111d` - → 압력/PCT/ΔP/front 서브시스템 전부 비활성 ### 추가 태그 (수집됨, FF 미사용) - `pi-6111b.pv` ≈ 리보일러 진공압 (탑저). `pica-6111.pv` = 탑정 진공압. **전탑 ΔP = pi-6111b − pica-6111**. - `ti-6103.pv` = 원료 프리히터 공급온도. `ficq-6115.pv` = 스팀 투입 flow(측정전용). `tica-6111a.op` = 스팀밸브 OP(밸브직결). - 태그 번호: P&ID는 10111, DCS는 6111 (동일계기, 10111→6111 치환). ### 물리 레이아웃 (위→아래, 패킹 3구간) ``` pica-6111 탑정 / REFLUX DIST(R) / TI-6111D / 상부PACKING(정류,D) / 제품추출 P(ficq-6118, 70~75%높이) / TI-6111C(제품 pivot) / 긴 중간PACKING(주분리,B front) / TI-6111B / PREHEATER DIST(피드,TI-6103) / 하부 짧은PACKING(stripping) / TICA-6111A 리보일러(pi-6111b≈여기) → B ``` ### 코드 색인 | 역할 | 경로 | |---|---| | 엔진(수정금지) | `src/Infrastructure/Control/FeedforwardEngine.cs` | | supervisor (스냅샷 빌드 참고) | `src/Infrastructure/Control/FeedforwardSupervisor.cs` (`BuildSnapshotAsync`) | | config store | `src/Infrastructure/Control/FeedforwardConfigStore.cs` (`LoadAllAsync`) | | 모델 | `src/Core/Application/Feedforward/FeedforwardModels.cs` (ColumnConfig/StreamConfig/PvSnapshot/TagSample) | | stores 인터페이스 | `src/Core/Application/Feedforward/IFeedforwardStores.cs` | | 컨트롤러 | `src/Web/Controllers/FeedforwardController.cs` (route `api/ff`, `GET dashboard`=`_store.GetAll()`) | | 실시간 조회 | `IExperionDbService.GetRealtimeRecordsByTagNamesAsync(tags)` (반환 row: TagName/LiveValue/Timestamp) | | DI 등록 | `src/Web/Program.cs` | | 프론트 | `src/Web/wwwroot/js/ff.js` | | 테스트 | `tests/ExperionCrawler.Tests/` | --- ## WP0 — Sim Override Layer (Claude 자율 테스트용 입력 변수) ### 목적 실행자(LLM)가 **운전원 없이 직접 입력값을 조정**해 시뮬레이션 루프를 자율 구동. 임의의 입력 태그(피드/압력/온도/스팀/스트림 PV)를 HTTP로 세팅 → advisor가 그 값을 읽음. **제어 쓰기 아님 — 입력 데이터 치환 계층.** ### 안전 원칙 (필수) 1. **OPC UA·제어 SP 쓰기 절대 없음**. 오버라이드는 *advisor가 읽는 입력값*만 in-memory로 대체. 2. **DB 쓰기 없음** (realtime_table 불변). 순수 in-memory 오버라이드 스토어. 3. **게이팅**: `appsettings`의 `Feedforward:SimOverrideEnabled=true`(기본 false, DEMO 전용)일 때만 동작. production 플래그면 엔드포인트 403. 4. 오버라이드 활성 시 advisory 응답/로그에 `simOverrideActive=true` 명시(은폐 금지). ### 설계 - **`SimOverrideStore`** (싱글톤, in-memory): `volatile bool Enabled`, `ConcurrentDictionary Values`. 메서드: `SetMany(dict)`, `Clear()`, `TryGet(tag,out v)`, `Snapshot()`. - **⚠ thread-safety 필수**: Singleton이므로 동시 HTTP 요청 간 Dictionary 경합 가능. `Dictionary`는 thread-safe하지 않음 → 반드시 `ConcurrentDictionary` 사용. `Enabled`도 ARM64 메모리 모델 고려해 `volatile` 또는 `Interlocked`로 보호. - 경로: `src/Infrastructure/Control/SimOverrideStore.cs` (+ 인터페이스 `ISimOverrideStore` in Core). - **읽기측 통합**: `FeedRampAdvisorService`(WP3-C)가 라이브 태그를 읽을 때, `SimOverrideStore.Enabled && TryGet(tag)` 이면 **오버라이드값 우선**(ts=now로 신선 처리), 없으면 live. 한 곳(태그 read 헬퍼)에서만 분기. - **엔드포인트** (`FeedforwardController`, DEMO 게이트): - `POST /api/ff/sim/override` body `{ "enabled":true, "values":{"ficq-6101.pv":1100,"pica-6111.pv":40,"pi-6111b.pv":80,"ti-6103.pv":150,"ficq-6115.pv":300} }` - `GET /api/ff/sim/override` → 현재 스냅샷 - `DELETE /api/ff/sim/override` → Clear + Enabled=false - **DI**: `SimOverrideStore` 싱글톤 등록(Program.cs). ### 산출물 - `ISimOverrideStore`/`SimOverrideStore`, 3개 엔드포인트, DI 등록, appsettings 플래그. ### 합격기준 - `SimOverrideEnabled=true`에서 `POST`로 ficq-6101.pv 세팅 → `GET /api/ff/ramp-advisor`가 그 값을 currentFeed로 사용. - `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용. - 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**. ### 한계 → 엔진 확장으로 일부 해소 (2026-06-01) 초기: Sim Override가 `FeedRampAdvisorService`(ramp-advisor)만 통합, 엔진 미반영. **확장 적용**: `FeedforwardSupervisor.BuildSnapshotAsync`의 `Sample`/`SampleExact`가 override 우선 → `/api/ff/advisory`(엔진 산출)도 override 반영. **안전가드**: `_sim.Enabled` 시 auto-write 억제(가짜 입력이 실제 OPC 쓰기 유발 방지). - **해소**: S7(mbState 과추출)·§10/front 물리검증은 이제 **override로 자율 가능**(B·temps 주입 → 다음 supervisor tick~2s 반영). - **여전히 불가**: S6(stale) — override는 항상 Good=fresh라 stale 시뮬 불가(단위테스트 `pv.Feed.Good=false`로 커버). P4(transient) — `FeedMoveThresholdPerMin=0`이라 config 임계 설정 필요(override만으론 미발화). ### 실행자 사용 예 (자율 루프) ``` curl -s -XPOST :5000/api/ff/sim/override -H 'Content-Type: application/json' \ -d '{"enabled":true,"values":{"ficq-6101.pv":900}}' curl -s ':5000/api/ff/ramp-advisor?columnId=1&targetFeed=1100&deltaIAllow=50' # → 기대치(WP1 S1)와 대조 curl -s -XDELETE :5000/api/ff/sim/override ``` --- ## WP1 — 검증 시나리오 매트릭스 (문서) ### 목적 WP3 구현 **전에** "입력값 → 기대 advisory 출력"을 명세. 이 문서가 곧 WP3의 구현 spec이자 합격기준(WP3 단위테스트 = 이 시나리오들). ### 선행조건 - §0.2 숙지, 브레인스토밍 §3·§7 공식 숙지. ### 산출물 - `docs/안전피드램프-검증시나리오매트릭스.md` ### 상세 단계 1. 시나리오 표 형식 정의(열): `ID | 목적 | 주입값(태그=값) | 기대 출력(ceiling/binding, rampRate/binding, rampTimeMin, steamTarget, warnings) | 합격기준`. 2. 아래 시나리오를 채울 것 (값·기대치 계산 포함). **숫자는 §0.2 config로 직접 계산해 명시**: | ID | 목적 | 주입 | 기대(요지) | |---|---|---|---| | S0 | baseline 평형 | 현 설정값(피드 ~900 등) | 변화요청 없음 → 현상태 표시, warning에 dormant 항목 | | S1 | 피드 증량(동특성 binding) | targetFeed=1100, 압력·조성 고정 | rampRate≈3.29(ΔI=50), binding=**dynamic**, rampTime≈61min, steam ×1.222 | | S2 | 밸브 슬루 binding | (가정) P rate_up=2 로 낮춤 + targetFeed=1100 | rampRate=2/0.95=2.1, binding=**valveSlew** | | S3 | ceiling 초과 목표 | targetFeed=2200 | clampedTarget=2105(=2000/0.95), binding=**P sp_max** | | S4 | flooding ceiling | ΔP(pi-6111b−pica-6111) 크게 주입 + floodLimit 설정 | ceiling이 flooding으로 더 낮아짐, binding=**flooding** | | S5 | TI-6103 하강(현열FF) | 피드 고정(900), ti-6103 150→140, sensibleGain·feedTempRef 지정 | steamTarget↑ = sensibleGain×900×(150−140) 만큼. 피드 무관 외란 FF | | S6 | stale/stall | 태그 timestamp frozen(>stale_sec) | advisory HOLD/무효 + warning=stale | | S7 | 과추출 감지(현행엔진) | B의 ficq-6116.pv 를 0.03×feed 대비 크게 주입, D+P+B≠feed | (현행) mbState "물질수지 불일치" 플래그 | 3. 각 시나리오에 **검증 방법**(주입 태그 목록 + 관찰 엔드포인트/SQL) 명기. 4. S1 worked example 계산을 부록으로 첨부(아래 §부록 공식 사용). ### 합격기준 - 7개 시나리오 모두 주입값·기대출력·합격기준·검증방법이 구체 수치로 채워짐. - 기대 수치가 §부록 공식과 일치(재현 가능). --- ## WP2 — 현행 FF 엔진 프로빙 (baseline 캡처) ### 목적 시뮬레이션으로 현행 엔진의 실제 거동을 캡처 → (a) 코드리딩 주장 검증, (b) WP3 전 baseline 확보, (c) dormant 서브시스템 동작상 확인. ### 선행조건 - 웹앱 가동(`:5000`), `GET /api/ff/advisory` 응답 확인. 운전원에게 값 주입 요청 가능 상태. ### 산출물 - `docs/안전피드램프-현행엔진-프로빙결과.md` (각 프로브: 주입값 → 관찰된 advisory 표) ### 상세 단계 (각 프로브 = 운전원 주입 요청 → dashboard/SQL 관찰 → 기록) | 프로브 | 주입 요청 | 관찰 포인트 | 예상(코드기준) | |---|---|---|---| | P0 | (없음) 현 상태 | dashboard 전체 | 각 스트림 recommendedSp/valid/grade, vloss/mbState 스냅샷 | | P1 | ficq-6101.pv 900→1100 | B(ficq-6116) recommendedSp | 0.03×ff로 **상승**, clamp/rate 미적용 확인 | | P2 | pica-6111.pv 변동 | transient/pUnstable | PressureBand=max라 **pUnstable 미발화**, PCT=raw(dtdp=0) | | P3 | pi-6111b.pv 변동 | advisory 전체 | **무반응**(미사용) 확인 | | P4 | ficq-6101.pv 급변 | c.transient, valid | transient=true, 스트림 valid=false | | P5 | D+P+B ≠ feed 주입 | mbState, vloss | "물질수지 불일치" | | P6 | 정상 온도프로파일 + front 부호 | frontPositionState/trimAdvice, temps | 정상 A>B>C>D 확인. **DiffTemp 부호버그(브레인스토밍 §10.2-A) 재현**: 프론트 상승/하강 라벨·환류↑/boilup↑ 권고가 반전됐는지. D의 환류 서브쿨 기여 측정 | ### 합격기준 - 6개 프로브 결과가 표로 기록됨. - P1·P2·P3가 코드리딩 주장(B 무제한 상승 / 압력 dormant / pi-6111b 미사용)을 **동작상 확인 또는 반증**. 반증 시 차이를 문서화. --- ## WP3 — Feed Ramp Advisor 구현 (read-only) ### 목적 목표 피드 입력 → **한계치(ceiling) · 램프율(rate) · 예상시간 · binding 제약 · 스팀(FIQ-6115) 목표**를 계산해 반환하는 advisory. **쓰기 없음**. WP1 시나리오를 통과해야 함. ### 설계 #### (A) 순수 계산기 (단위테스트 대상) — `FeedRampCalculator` 신규: `src/Infrastructure/Control/FeedRampCalculator.cs`. **순수 함수**(부수효과 0)로 작성해 테스트 용이하게. ``` public static FeedRampAdvisory Compute( ColumnConfig cfg, PvSnapshot pv, // 현재 PV (feed/streams/pressure 등) double targetFeed, double deltaIAllow, // 허용 순간 불균형 kg/hr (기본 50) double sensibleGain, // (a) 현열 게인 ≈ Cp_feed/λ_steam [kg스팀/(kg피드·°C)]. NaN/0=현열보정 off+warning double feedTempRef, // (a) 앵커 기준 feed 온도(평형). NaN=현열보정 off+warning RampExtraInputs extra); // pica/pi6111b/ti6103/fiq6115/op 등 부가 태그 값 (옵션) ``` 계산 순서: 1. `currentFeed = pv.Feed.Value` (Good 아니면 HOLD 반환 + warning=feed-bad). 2. **Ceiling**: - `valveCeiling = min over (Commanded ∪ K>0 streams) of (sp_max_i / K_i)`; binding=해당 key. - `floodingCeiling`: extra에 pica·pi6111b 둘 다 있고 `floodLimit`(파라미터/옵션) 있으면: `ΔPnow=pi6111b−pica`; `feedFlood=currentFeed×(floodLimit/ΔPnow)^(1/n)` (n 기본 1.8). 없으면 skip + warning="flooding ceiling 미산정(ΔP/limit 부재)". - `steamCeiling`: 산정 근거(최대 OP/스팀) 없으면 skip + warning. - `ceiling = min(존재하는 것들)`; binding 라벨. 3. **Ramp rate** (kg/hr per min): - `(a) valveSlew = min over Commanded (rate_up_i / K_i)` - `(b) dynamic = deltaIAllow × 60 / (K_P × (τ_P + θ_P))` (P=ProductKey, θ_P=ThetaUpSec, τ_P=TauSec) - `(c) energyLoop`: 에너지루프 시상수 입력 있으면 산정, 없으면 skip + warning. - `R_feed = min(존재)`; binding 라벨. 4. `clampedTarget = min(targetFeed, ceiling)`. 5. `rampTimeMin = max(0, (clampedTarget − currentFeed)) / R_feed` (target0→steam↑. - startOP 제안은 local gain 부재 시 omit + warning. 7. `warnings[]`: dormant/부재 항목 전부 적재(dtdp=0 PCT off, pi-6111b 미사용, flood/energy/steam 근거 부재 등). #### (B) DTO — `src/Core/Application/Feedforward/FeedRampModels.cs` record + `[JsonPropertyName]` camelCase: ``` FeedRampAdvisory(columnId, currentFeed, targetFeed, clampedTarget, Bound ceiling, Bound rampRate, double rampTimeMin, SteamTarget steam, bool simOverrideActive, string[] warnings) Bound(double value, string binding) // binding 예: "valveSlew@P","dynamic","P sp_max","flooding" SteamTarget(double? fiq6115From, double? fiq6115To, double? startOpPct) ``` #### (C) 서비스 — `FeedRampAdvisorService` 신규 `src/Infrastructure/Control/FeedRampAdvisorService.cs`. 라이브 데이터 수집 + 계산기 호출. - 주입: `IFeedforwardConfigStore`, `IExperionDbService`, **`ISimOverrideStore`(WP0)**. - `LoadAllAsync`로 cfg 찾기 → 필요한 태그 목록(feed/streams/pica-6111/pi-6111b/ti-6103/ficq-6115/tica-6111a.op) 구성 → `GetRealtimeRecordsByTagNamesAsync` → `PvSnapshot`+`RampExtraInputs` 빌드(스냅샷 빌드는 `FeedforwardSupervisor.BuildSnapshotAsync` 패턴 참고, **단 복붙 말고 필요한 태그만**) → `FeedRampCalculator.Compute` 호출. - **태그 read 헬퍼 단일 분기**: `SimOverrideStore.Enabled && TryGet(tag)` 면 오버라이드값(ts=now), 아니면 live. 응답에 `simOverrideActive` 플래그 전달. - 신선도: timestamp가 stale_sec 초과면 해당 태그 Good=false → 계산기에서 HOLD/warning. #### (D) 엔드포인트 — `FeedforwardController` 신규 `[HttpGet("ramp-advisor")]`: ``` GET /api/ff/ramp-advisor?columnId=1&targetFeed=1100&deltaIAllow=50&floodLimit=&n=1.8&sensibleGain=&feedTempRef= → FeedRampAdvisory (JSON camelCase) - sensibleGain·feedTempRef: 미전달 시 현열보정 off(+warning). config 필드화는 후속(현재 query/기본). - ⚠ `floodLimit` query param과 `ColumnConfig.DeltaPFloodLimit`(DB 저장) 중복: query param이 config보다 우선(config 있으면 config 값이 기본; query param 전달 시 override). 문서화 필수. ``` - 읽기 전용. 컨트롤러에 `FeedRampAdvisorService` 주입(생성자 추가). #### (E) DI 등록 — `Program.cs` `FeedRampAdvisorService`를 scoped 등록(다른 FF 서비스 등록부 근처). #### (F) 단위테스트 — `tests/ExperionCrawler.Tests/FeedRampCalculatorTests.cs` - WP1 시나리오 S1~S5를 `FeedRampCalculator.Compute`에 대한 단위테스트로 작성(합성 ColumnConfig + PvSnapshot 직접 구성, 라이브 불요). - **S5는 `sensibleGain`·`feedTempRef` 인자를 명시 전달**해 현열보정 검증(부록A S5 예제: sensibleGain=0.001, feedTempRef=150, ti6103=140 → steam +9.0). - S7(물질수지 불일치)은 **`FeedRampCalculator` 대상 아님** — mbState는 `FeedforwardEngine`에서만 산출. S7은 WP2(현행엔진 프로빙) 전용. WP3에 포함하려면 통합테스트(`FeedforwardEngine` 경유)로 별도 작성. - 예) S1: C-6111 config + currentFeed=900, targetFeed=1100, ΔI=50 → `rampRate.value≈3.29`(허용오차), `rampRate.binding=="dynamic"`, `rampTimeMin≈60.8`, `ceiling.value≈2105`, `ceiling.binding` P 관련, `steam.fiq6115To≈fiq6115From×1.2222`. ### 상세 단계 (순서) 1. (B) DTO 작성 → 빌드. 2. (A) 계산기 작성 → 빌드. 3. (F) 단위테스트 작성 → `dotnet test` 통과(= WP1 충족) 까지 (A) 보정. 4. (C) 서비스 → (E) DI → (D) 엔드포인트. 5. 빌드 경고 0/에러 0. 6. (시뮬레이션) 운전원에 S1 값 주입 요청 → `GET /api/ff/ramp-advisor?columnId=1&targetFeed=1100` 응답이 WP1 기대치와 일치 확인. 7. (선택, WP3b) `ff.js`에 "피드 램프 계산기" 패널(입력칸+결과표) 추가 — 별도 승인 후. ### 합격기준 - `dotnet build` 경고 0 / 에러 0, `dotnet test` 전건 통과. - WP1 시나리오 S1~S5 단위테스트 통과. (S7은 Engine 레벨 통합테스트로 별도 — 이 문서 범위 외.) - 엔드포인트가 S1 라이브 주입에 대해 기대 JSON 반환(rampRate binding=dynamic, time≈61min, steam ×1.22). - 쓰기(OPC/DB) 호출 **0건** (코드 grep으로 확인: WriteTagAsync/Insert/Update 미사용). - 모든 미산정 항목이 `warnings[]`에 노출(은폐 금지). ### 비범위 (이번 WP3 제외 — 후속) - 편차 trim / sensitive tray 2-point / 압력 서브시스템 깨우기 / 에너지루프 시상수 식별 / 자동쓰기. (브레인스토밍 §7.5·§8·§9 — 별도 작업지시서.) --- ## 부록 A. 공식 요약 (재현용) ``` [Ceiling] valveCeiling = min_i( sp_max_i / K_i ) floodingCeiling = currentFeed × (floodLimit / ΔPnow)^(1/n), ΔPnow = pi6111b − pica6111 ceiling = min(available) [Ramp rate, kg/hr per min] (a) valveSlew = min_i( rate_up_i / K_i ) (Commanded) (b) dynamic = ΔI_allow × 60 / (K_P × (τ_P + θ_P)) (P=ProductKey, 초 단위 τ,θ) (c) energyLoop= (에너지루프 시상수 기반, 입력시) R_feed = min(available) [Time] rampTimeMin = (clampedTarget − currentFeed) / R_feed [Steam] fiq6115To = fiq6115Cur × (clampedTarget/currentFeed) + sensibleGain × clampedTarget × (feedTempRef − ti6103Now) // (a) 현열보정 sensibleGain ≈ Cp_feed/λ_steam [kg스팀/(kg피드·°C)]; feedTempRef=앵커 feed온도 ``` ### C-6111 worked example (S1: 900→1100, ΔI=50) ``` valveSlew = 30/0.95 = 31.6 dynamic = 50×60 / (0.95×(900+60)) = 3000/912 = 3.29 ← binding R_feed = 3.29 kg/hr·min rampTime = (1100−900)/3.29 = 60.8 min valveCeiling = min(2000/0.95, 2000/0.8, 1000/0.02, 500/0.03) = 2105 (P) steam = fiq6115Cur × 1100/900 = ×1.2222 (S1: ti6103=feedTempRef → 현열항 0) ``` ### C-6111 worked example (S5: 피드 900 고정, TI-6103 150→140, sensibleGain=0.001, feedTempRef=150) ``` 처리량비례 = fiq6115Cur × 900/900 = fiq6115Cur (변화 0) 현열보정 = 0.001 × 900 × (150 − 140) = 9.0 kg/hr fiq6115To = fiq6115Cur + 9.0 ← steam↑ (피드 변화 없이 TI-6103 외란만으로) ※ sensibleGain=0.001은 DEMO placeholder. 실제는 Cp_feed/λ_steam로 캘리브레이션. ``` ## 부록 B. 진단 결과 (2026-06-01, diagnosis-checklist.md §1~§8 수행) ### 발견된 문제점 | # | 항목 | 등급 | 상태 | |---|------|------|------| | 1 | `SimOverrideStore`에 `ConcurrentDictionary` + `volatile bool` 미명시 — Singleton 동시성 경합 위험(ARM64) | 🟠 MED | WP0 §96에 수정 반영 | | 2 | S7(mbState)를 `FeedRampCalculator.Compute` 단위테스트 대상으로 지정 — S7은 Engine 전용, Calculator로 테스트 불가 | 🟠 MED | WP3 §253·§267 수정 반영 | | 3 | `floodLimit` query param과 `ColumnConfig.DeltaPFloodLimit`(DB) 이중 소스 — 우선순위 불명확 | 🟡 LOW | WP3 §244에 주석 추가 | | 4 | `appsettings.json`에 `Feedforward:SimOverrideEnabled` 섹션 없음 — 구현 시 누락 주의 | 🟡 LOW | 사전 공유로 대체 | | 5 | `RampExtraInputs`가 C-6111 태그 hardcode — 타 컬럼 확장 시 ColumnConfig 확장 필요 | 🟡 LOW | 현 스코프(C-6111) 한정 | | 6 | **S5(스팀/TI-6103)가 #2와 동형 모순** — 스팀 1차식은 피드 고정 시 변화=0인데 S5는 steam↑ 기대. 현열부하는 앵커링으로 소거 불가 → 별도 게인 필수 | 🟠 MED | **해소 (a 확정)** | ### #6 해소 = (a) 확정 (2026-06-01) - **`sensibleGain`(≈Cp/λ_steam) + `feedTempRef`(앵커 feed온도) 파라미터 도입**. 현열보정 = `sensibleGain × clampedTarget × (feedTempRef − ti6103Now)`. - **T_b 소거 유도**: 전량식에서 처리량비례항을 빼면 잔차 = `(Cp/λs)·F·(T0−T1)` → 비점 불필요, 게인 1개로 충분. - S5 mandatory 유지(WP3-F·§270). 두 파라미터 NaN/0이면 현열항 0 + warning(안전 기본). - 반영: Compute 시그니처(§198~), step6(§220~), 엔드포인트 query param, 부록A 공식·S5 예제. ### 진단 보강 (Opus 재검토) - **#1 보강**: `ConcurrentDictionary`는 per-key 안전만 보장. `SetMany`+`Snapshot` 그룹 원자성은 미보장 → 일관된 다중태그 스냅샷이 필요하면 lock 또는 immutable-dict swap. (순차 테스트 사용엔 무방.) - **#5 보강**: `steam_op_tag`는 ColumnConfig에 이미 존재 → 그건 config에서 읽고, config에 없는 태그(pi-6111b/ti-6103/ficq-6115)만 하드코딩+TODO. - **(b) 공식 일반화(minor)**: dynamic 제약은 P만이 아니라 lagging Commanded 전체 합 `ΔI×60/Σ K_i(τ_i+θ_i)`이 정확. C-6111은 P만 τ≠0이라 결과 동일. ### 교차 검증 결과 (STEP 6) - 이미 수정된 문제(Q1): 없음 - 다른 레이어에서 처리(Q2): 없음 - 의도적 설계(Q3): #4·#5는 Phase I 스코프 한정으로 의도적 - 재현 불가(Q4): 없음 - **누락(Opus 추가)**: #6 (S5/스팀 현열항 모순) — 진단 체크리스트가 #2(S7)는 잡았으나 동형인 S5는 놓침 --- ## 부록 C. 참조 문서 - `docs/안전피드램프-한계치-브레인스토밍.md` (설계 전문 §0~§9) - 메모리: `project_safe_feed_ramp_brainstorm`, `project_demo_system_synthetic_data`, `reference_json_serializer_pascalcase`, `project_sidedraw_ff_advisory`