- 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>
25 KiB
작업지시서 — 안전 피드램프 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 적용, 위반 금지)
- 쓰기 절대 금지: OPC UA 쓰기·DB 쓰기 없음. 현 시스템은 AdvisoryOnly 정책. WP3는 순수 계산 + 조회만.
- DEMO 시스템: 모든 실시간 값은 운전원이 주입한 합성값(테스트 벤치). 로직/기능 검증엔 유효하나, 데모 값으로 실제 플랜트 τ/θ·플러딩 임계를 단정하지 말 것. (메모리
project_demo_system_synthetic_data참조.) - 시뮬레이션 절차 (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으로 대체).
- (자율, 권장) Sim Override — WP0에서 만드는 in-app 입력 오버라이드. 실행자(LLM)가
- JSON 직렬화: 신규 DTO 반환 시 PascalCase 패스스루 주의 →
[JsonPropertyName("camelCase")]명시 (메모리reference_json_serializer_pascalcase). 기존 컨트롤러는 익명객체로 수동 camelCase 처리하는 패턴도 있음 —GetDashboard응답 빌더 참고. - 빌드/테스트 명령:
- 빌드:
dotnet build src/Web/ExperionCrawler.csproj→ 경고 0 / 에러 0 필수 - 테스트:
dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj
- 빌드:
- 건드리지 말 것: 기존
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가 그 값을 읽음. 제어 쓰기 아님 — 입력 데이터 치환 계층.
안전 원칙 (필수)
- OPC UA·제어 SP 쓰기 절대 없음. 오버라이드는 advisor가 읽는 입력값만 in-memory로 대체.
- DB 쓰기 없음 (realtime_table 불변). 순수 in-memory 오버라이드 스토어.
- 게이팅:
appsettings의Feedforward:SimOverrideEnabled=true(기본 false, DEMO 전용)일 때만 동작. production 플래그면 엔드포인트 403. - 오버라이드 활성 시 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(+ 인터페이스ISimOverrideStorein Core).
- ⚠ thread-safety 필수: Singleton이므로 동시 HTTP 요청 간 Dictionary 경합 가능.
- 읽기측 통합:
FeedRampAdvisorService(WP3-C)가 라이브 태그를 읽을 때,SimOverrideStore.Enabled && TryGet(tag)이면 오버라이드값 우선(ts=now로 신선 처리), 없으면 live. 한 곳(태그 read 헬퍼)에서만 분기. - 엔드포인트 (
FeedforwardController, DEMO 게이트):POST /api/ff/sim/overridebody{ "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
상세 단계
- 시나리오 표 형식 정의(열):
ID | 목적 | 주입값(태그=값) | 기대 출력(ceiling/binding, rampRate/binding, rampTimeMin, steamTarget, warnings) | 합격기준. - 아래 시나리오를 채울 것 (값·기대치 계산 포함). 숫자는 §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 "물질수지 불일치" 플래그 |
- 각 시나리오에 검증 방법(주입 태그 목록 + 관찰 엔드포인트/SQL) 명기.
- 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 등 부가 태그 값 (옵션)
계산 순서:
currentFeed = pv.Feed.Value(Good 아니면 HOLD 반환 + warning=feed-bad).- 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 라벨.
- 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 라벨.
clampedTarget = min(targetFeed, ceiling).rampTimeMin = max(0, (clampedTarget − currentFeed)) / R_feed(target<current면 down-ramp: rate_dn 사용 분기 + 별도 표기).- Steam target (extra.fiq6115 있을 때) — (a)안 현열 보정 포함:
fiq6115To = fiq6115Cur × (clampedTarget / currentFeed) // 처리량 비례(앵커 온도 기준) + sensibleGain × clampedTarget × (feedTempRef − ti6103Now) // 현열 보정(앵커 대비 편차)- T_b 소거 유도: 전량식
FIQ=[F·Cp·(T_b−T_feed)+c·F]/λs에서 비례항을 빼면 잔차 =(Cp/λs)·F·(T0−T1)→ 비점 불필요,sensibleGain만으로 충분. sensibleGain또는feedTempRef가 NaN/0이면 현열항=0 + warning="현열보정 off".ti6103Now=extra의 ti-6103 값.- S1(온도 고정: ti6103Now≈feedTempRef)→편차≈0→순수 비례. S5(피드 고정·온도↓)→편차>0→steam↑.
- startOP 제안은 local gain 부재 시 omit + warning.
- T_b 소거 유도: 전량식
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경유)로 별도 작성.
- S5는
- 예) S1: C-6111 config + currentFeed=900, targetFeed=1100, ΔI=50 →
rampRate.value≈3.29(허용오차),rampRate.binding=="dynamic",rampTimeMin≈60.8,ceiling.value≈2105,ceiling.bindingP 관련,steam.fiq6115To≈fiq6115From×1.2222.
상세 단계 (순서)
- (B) DTO 작성 → 빌드.
- (A) 계산기 작성 → 빌드.
- (F) 단위테스트 작성 →
dotnet test통과(= WP1 충족) 까지 (A) 보정. - (C) 서비스 → (E) DI → (D) 엔드포인트.
- 빌드 경고 0/에러 0.
- (시뮬레이션) 운전원에 S1 값 주입 요청 →
GET /api/ff/ramp-advisor?columnId=1&targetFeed=1100응답이 WP1 기대치와 일치 확인. - (선택, 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