Files
ExperionCrawler/plans/안전피드램프-advisory-작업지시서.md
windpacer 60946f3c47 feat: Sim Override를 FF 엔진까지 확장 (S7/§10/front 자율검증)
- FeedforwardSupervisor.BuildSnapshotAsync Sample/SampleExact: override 우선(신선) → /api/ff/advisory(엔진)도 override 반영
- 안전가드: _sim.Enabled 시 auto-write 억제(가짜 입력→실제 OPC 쓰기 방지)
- 해소: S7(mbState)·§10/front 자율검증 가능. 잔여: S6(override=fresh)·P4(FeedMoveThresholdPerMin=0)
- 작업지시서 WP0 한계 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:30:32 +09:00

371 lines
25 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.
# 작업지시서 — 안전 피드램프 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<string,(double value,DateTime ts)> 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-6111bpica-6111) 크게 주입 + floodLimit 설정 | ceiling이 flooding으로 더 낮아짐, binding=**flooding** |
| S5 | TI-6103 하강(현열FF) | 피드 고정(900), ti-6103 150→140, sensibleGain·feedTempRef 지정 | steamTarget↑ = sensibleGain×900×(150140) 만큼. 피드 무관 외란 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=pi6111bpica`; `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` (target<current면 down-ramp: rate_dn 사용 분기 + 별도 표기).
6. **Steam target** (extra.fiq6115 있을 때) — (a)안 현열 보정 포함:
```
fiq6115To = fiq6115Cur × (clampedTarget / currentFeed) // 처리량 비례(앵커 온도 기준)
+ sensibleGain × clampedTarget × (feedTempRef ti6103Now) // 현열 보정(앵커 대비 편차)
```
- **T_b 소거 유도**: 전량식 `FIQ=[F·Cp·(T_bT_feed)+c·F]/λs`에서 비례항을 빼면 잔차 = `(Cp/λs)·F·(T0T1)` → 비점 불필요, `sensibleGain`만으로 충분.
- `sensibleGain` 또는 `feedTempRef`가 NaN/0이면 현열항=0 + warning="현열보정 off". `ti6103Now`=extra의 ti-6103 값.
- S1(온도 고정: ti6103Now≈feedTempRef)→편차≈0→순수 비례. S5(피드 고정·온도↓)→편차>0→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 = (1100900)/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·(T0T1)` → 비점 불필요, 게인 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`