diff --git a/docs/안전피드램프-한계치-브레인스토밍.md b/docs/안전피드램프-한계치-브레인스토밍.md new file mode 100644 index 0000000..970beb4 --- /dev/null +++ b/docs/안전피드램프-한계치-브레인스토밍.md @@ -0,0 +1,497 @@ +# 안전 피드 램프 · 운전 한계치 — 브레인스토밍 + +> 2026-06-01, C-6111 PGMEA 측류 추출식 증류탑 기준 +> 상태: **보류(brainstorming)** — 구현 착수 전 헛점 검토 단계 + +## 0. 배경 / 발단 + +선행 논의에서 출발: +1. FF의 LevelDriven 스트림(D=ficq-6114, B=ficq-6116)이 `recommendedSp = K×피드`를 **"권장 SP" 컬럼에 상승값으로 그대로 표시** → 운전원이 추종 시 *피드↑ → 레벨↑ → 드로우 SP↑ → 중비물(B) 일시 과추출* 을 시스템이 부추기는 구조라는 지적. +2. "advisory라 안전"은 근거 없는 합리화 (advisory 라벨은 그 숫자의 타당성을 보증하지 않음). +3. → **관점 전환**: "권장값을 어떻게 표시하나"가 아니라, **"피드를 평형 깨지 않고 얼마나·얼마나 빨리 올릴 수 있나"를 계산**하는 쪽으로. + +## 1. 문제 정의 — 두 개를 분리 + +| | 정체 | 성격 | 예시 질문 | +|---|------|------|-----------| +| **한계치 (ceiling)** | 평형 깨지 않는 **최대 피드** | 정적 상한 (level) | "1100이 애초에 가능한가?" | +| **RATE 한계치 (ramp)** | 900→1100을 얼마나 **빨리** | 동적 상한 (rate) | "몇 분에 걸쳐 올려야 안전한가?" | + +사용자 원안: 900 → 1100 (Δ=200), RATE LIMIT + 시간 고려한 **점진 SP 상승 안전 램프**. + +## 2. C-6111 실제 config (ff_stream_config, column_id=1) + +| 스트림 | flow_tag | 역할 | K | θ_up/dn | τ | sp_max | rate_up/dn (kg/hr·min) | level_tag | +|--------|----------|------|-----|---------|------|--------|------------------------|-----------| +| P | ficq-6118 | Commanded | 0.95 | 60/60 | **900** | 2000 | 30/60 | – | +| R | ficq-6113 | Commanded | 0.80 | 0/0 | 0 | 2000 | 30/30 | – | +| D | ficq-6114 | LevelDriven | 0.02 | – | – | 1000 | 0/0 | lica-6113 | +| B | ficq-6116 | LevelDriven | 0.03 | – | – | 500 | 0/0 | li-6111 | + +피드 = ficq-6101. (K 합 P+D+B = 0.95+0.02+0.03 = 1.00 → 물질수지 폐합. R은 내부 환류.) + +## 3. 계산 프레임워크 + +### 3.1 RATE 한계치 (램프율) + +램프율 `R_feed`(kg/hr per min)에 걸리는 두 제약 중 **작은 쪽**이 binding: + +**(a) 밸브 슬루 제약** — 각 commanded 드로우가 제 rate 한계 안에서 피드를 추종: +``` +R_feed ≤ min_i( rate_up_i / K_i ) = min(30/0.95, 30/0.80) = 31.6 kg/hr·min (P binding) +→ 200 kg/hr 를 약 6.3분 +``` + +**(b) 동적 불균형 제약** — P의 지연(τ=900s + θ=60s)이 만드는 물질수지 결손: +``` +ΔI(결손, kg/hr) ≈ K_P · (τ_P + θ_P) · R_feed + = 0.95 · 960s · (R_feed/60) ≈ 15.2 · R_feed[kg/hr·min] +``` +이 결손이 컬럼에 누적 → 레벨 상승 → D·B 과추출. 허용 밴드 ΔI_allow로 역산: +``` +R_feed ≤ ΔI_allow / 15.2 + ΔI_allow = 50 kg/hr → R_feed ≤ 3.3 → 200kg/hr 에 약 60분 + ΔI_allow = 100 → R_feed ≤ 6.6 → 약 30분 +``` + +**핵심 결론**: 밸브 속도론 "6분"이지만, P의 15분 지연 때문에 내부 균형 유지 시 **~1시간 램프**가 맞다. binding은 (b) 동특성이지 (a) 밸브가 아니다. `ΔI_allow`는 운전원이 정하는 손잡이(= 허용 레벨 스윙/과추출량). + +### 3.2 한계치 (ceiling) + +**밸브 포화 상한** (config만으로): +``` +feed_max = min_i( sp_max_i / K_i ) + P: 2000/0.95 = 2105 ← binding + R: 2000/0.80 = 2500 + D: 1000/0.02 = 50000 + B: 500/0.03 = 16667 +→ 1100은 여유 충분 (P_sp=1045, R=880, D=22, B=33 모두 sp_max 이내) +``` +단, **진짜 상한은 더 낮음** (라이브 데이터 필요): +- **플러딩**: 내부 traffic ↑ → ΔP ↑. `DeltaPFloodLimit` 대비 투영 +- **리보일러/스팀**: `SteamOpTag` OP%가 포화(>~95%)하면 분리 붕괴 + +### 3.3 엔진 통합 — 자기조절(self-throttling) 램프 + +기존 FF 엔진은 "주어진 피드에서 각 스트림 기대치"를 이미 계산. 그 위에 **Feed Ramp Advisor**: +1. 목표 피드 타당성 검사(ceiling) → 초과 시 클램프 + 제한 제약 보고 +2. (a)(b)로 R_feed 산출 → 궤적 `feed(t)=900+R_feed·t` +3. 램프 중 레벨·ΔP·프론트 감시 → 밴드 접근 시 R_feed 자동 감속/정지 + +--- + +## 4. ⚠ 헛점 / 리스크 분석 (이 문서의 핵심) + +### A. 모델 신뢰성 헛점 + +| # | 헛점 | 영향 | 비고 | +|---|------|------|------| +| A1 | **θ·τ가 실측인가 placeholder인가** | R_feed 전부가 이 값에 비례 → 틀리면 램프 전체 오류 | 특히 R의 θ=0/τ=0은 비현실(환류 즉답 가정). config가 추정치일 가능성 큼 → `CrossCorrLagEstimator` 실측으로 교체 필요 | +| A2 | **1차 선형 lag 가정** | deficit 공식은 선형. 증류탑은 비선형(분리효율·게인이 부하 의존) | 22% step은 근사 OK일 수 있으나 큰 변화엔 부정확 | +| A3 | **운전점 의존 K** | 램프 계산에 config K vs 실측 K_obs 어느 것? 운전점마다 다름 | K_obs MA 기능 이미 존재 → 그쪽 사용 검토 | + +### B. 모델이 빠뜨린 물리 (가장 위험) + +| # | 헛점 | 영향 | 비고 | +|---|------|------|------| +| B1 | **D·B 제약 미반영** | 정작 우려한 **과추출의 주체가 D·B level loop**인데, binding을 P commanded로만 계산. D·B의 rate_up=0(미설정) → 밸브 포화·레벨 루프 속도 한계가 모델에 없음 | **모순**: P 지연이 만든 결손을 D·B가 흡수(과추출)하는데 D·B 자체 한계는 무시 | +| B2 | **에너지 동특성 누락** | 안전 램프 전제 = "피드 올릴 때 reboil/reflux도 비례 상승". R은 commanded지만 **리보일러/스팀은 monitor만**(commanded 아님). 스팀이 못 따라가면 분리 붕괴·프론트 드리프트 | 램프 중 듀티 자동조정 없으면 공식 무의미 | +| B3 | **R의 P 종속 연쇄지연** | R=R_f×P_sp(외부환류, P 지연 상속) → 램프 중 reflux도 지연 → 분리 약화 → 프론트 | 연쇄 lag, 단일 τ로 표현 안 됨 | +| B4 | **결손→레벨 환산 미정** | deficit(kg/hr)를 레벨 스윙(%)으로 환산하려면 홀드업·단면적·밀도 필요. 없으면 ΔI_allow를 kg/hr로 줘야 해 비직관적 | 운전원은 레벨(%)로 주고 싶을 것 | + +### C. 동적/과도 안전 헛점 + +| # | 헛점 | 영향 | 비고 | +|---|------|------|------| +| C1 | **transient 플러딩** | 정적 ceiling 체크는 램프 중 순간 ΔP 스파이크(reflux 일시 과잉) 못 잡음 | self-throttle이 부분 보완하나 ΔP 측정 자체 지연 존재 | +| C2 | **감시 stall 시 폭주** | self-throttle은 라이브 레벨/ΔP에 의존. realtime stall(과거 실제 발생) 시 감시가 멀어버림 | **램프는 신선도 게이트 + stall 시 즉시 freeze 필수**. corroboration 재사용 | +| C3 | **방향 비대칭 미처리** | up(부하↑)과 down 동특성 다름(P rate 30↑/60↓). 공식은 up만 | down ramp 별도 유도 필요 | +| C4 | **램프완료 ≠ 신평형** | 1100 도달 후에도 τ=900s 때문에 ~15분 더 정착. "완료"와 "평형 도달" 구분 필요 | 정착 판정 기준 미정 | +| C5 | **동시 외란** | 램프 중 압력변동·조성변화·상류 트립 겹치면 "다른 건 정상" 전제 붕괴 | transient detection 있으나 램프 vs 외란 구분 필요 | + +### D. 운전/권한 헛점 + +| # | 헛점 | 영향 | 비고 | +|---|------|------|------| +| D1 | **피드 쓰기는 AdvisoryOnly 정면 위반** | 현재 시스템은 advisory-only 정책. 자동 램프 쓰기는 WriteGuard·운전원 ARM·rate clamp 전부 통과해야 | **보고(advisory)와 실행(write) 명확 분리** 필수 | +| D2 | **피드를 이 탑이 제어하나?** | ficq-6101이 상류 공정/탱크에서 오면 이 탑이 임의 ramp 불가. 피드 SP 쓰기 권한·상류 제약 확인 안 됨 | 미확인 — 현장 확인 필요 | + +--- + +## 5. 미해결 / 필요 데이터 + +- D·B 밸브 최대 드로우율 (현재 config 0) 또는 레벨 루프 게인 +- 라이브 ΔP(`DeltaPTag`)·스팀 OP(`SteamOpTag`) → flooding/듀티 ceiling +- 홀드업·단면적·밀도 → 결손 kg/hr ↔ 레벨 % 환산 +- θ·τ 실측치 (estimator vs config 검증) +- 피드(ficq-6101) 쓰기 권한·상류 제약 (현장) + +## 6. 다음 단계 (모두 보류 상태) + +- **(1)** 공식을 **계산기 advisory**로 구현 (목표 피드 입력 → 한계치/램프율/예상시간/binding 제약 표시, 쓰기 없음) +- **(2)** 데이터 식별 θ/τ(`CrossCorrLagEstimator`) 연동 +- **(3)** 라이브 ΔP·스팀으로 flooding/듀티 ceiling 포함 +- **(우선)** 종이 위 손계산 — 현재 라이브 피드/ΔP/스팀으로 실제 한계치·램프 1회 산출해 숫자 감각 확보 + +> 헛점 B1(D·B 제약 미반영)·B2(에너지 동특성)·C2(stall freeze)·D1(쓰기 분리)이 가장 시급한 검토 대상. + +## 7. 추가 브레인스토밍 — 자기조절 캐스케이드 + (목표제거량 + 편차) SP 구조 (2026-06-01 b) + +### 7.1 실제 물리 캐스케이드 (운전원 정정) + +``` +FICQ-6101(피드)↑ + → LI-6111(리보일러 레벨)↑ + → TICA-6111A 온도↓ (액 증가/희석) + → 스팀밸브 출력↑ → 스팀투입↑ + → 온도↑ → 증발량↑ + → LI-6111 레벨 유지 +``` + +**핵심 정정**: LI-6111(리보일러 레벨)은 **B 드로우가 아니라 리보일러 듀티(스팀, TICA-6111A 온도 캐스케이드)로 유지**된다. → 레벨은 *에너지(증발)*, B(중비물)는 *조성 목표* 로 잡는 **decoupled 구조**. 피드 증가가 B 과추출을 강제하지 않는다. + +### 7.2 선행 헛점에 주는 영향 + +| 선행 헛점 | 캐스케이드 반영 후 | +|---|---| +| **B1** (D·B 과추출 강제) | **부분 해소** — 레벨은 듀티가 잡음. 단 FF config가 B를 `LevelDriven by li-6111`로 둔 것은 **모델 오류 의심**(B는 실제 조성구동) → §7.4 | +| **B2** (에너지 동특성 "수동") | **재정의** — 스팀은 수동 아님. TICA-6111A 온도 캐스케이드로 **자동 추종**(closed loop). 단 이 루프도 *자체 lag* 보유 → 새 binding 제약 §7.3 | + +### 7.3 새 binding 제약 — 리보일러 에너지 루프 대역폭 + +자기조절 캐스케이드는 피드 증가를 **자동 흡수**하지만, 온도/스팀/증발 루프는 자체 시상수(스팀 동특성 + 리보일러 열관성 + 온도측정 지연)를 가진다. 피드를 이보다 빨리 올리면: +- TICA-6111A 온도 언더슈트 → 분리 약화(중비물이 P로 상승 / P 오염) +- LI-6111 레벨 스윙 + +→ 램프율 제약에 **(c) 에너지 루프 제약** 추가. binding = (a)밸브슬루 · (b)생성물 지연결손 · **(c)에너지 루프** 중 **최솟값**. + +**유추 검증**: *최대 피드 RATE 기준으로 SP를 움직이면 자기조절 캐스케이드가 평형 유지하며 증가 피드에 대응* → **맞다.** 단 그 "최대 RATE"는 (a)(b)(c) 중 가장 느린 루프가 결정. + +### 7.4 SP 구조 정정 — (목표제거량) + (advisory 편차) + +**SP = 제거 목표량 + advisory 편차** → **표준 피드포워드(조성×피드) + 피드백 트림(편차) 구조. 맞다.** + +``` +B_SP(중비물) = [중비물분율(원료분석) × 피드] + 편차_trim +D_SP(경비물) = [경비물분율(원료분석) × 피드] + 편차_trim +``` + +| 항 | 정체 | 비고 | +|---|------|------| +| **제거 목표량 (base)** | 원료분석 조성분율 × 피드율 | 현재 고정 K(B 0.03 / D 0.02) 대신 **실제 원료분석값**. 분석 갱신 시 갱신 | +| **편차 trim (advisory)** | 물질수지 오차 / 조성 드리프트 / 프론트 보정 | **반드시 clamp + rate limit (bounded)** | + +**원래 불만 정면 해결**: +- 기존 `B_SP = K×피드` (무제한 상승, 레벨 추종) → 폐기 +- 신규 `B_SP = 분석목표 + bounded 편차` → base는 물질수지 정합(피드포워드), 편차는 유계 트림(피드백). **runaway 없음.** + +### 7.5 7.4가 남기는 미해결 + +- **편차 소스 정의**: 무엇으로? (a) vloss 중 B 귀속분 (b) 조성분석기 드리프트 (c) 프론트 위치 — 측정 가능성 확인 +- **원료분석 입력 경로**: 분율은 어디서? (랩 수동입력 / 온라인 분석기 / KB) · 갱신 주기 +- **config 모델 정정**: B·D를 `LevelDriven` → `Commanded(조성구동)` 재분류? li-6111 level_tag은 모니터로 강등? → **현장 확인 필수** (li-6111 실제 제어 주체가 듀티인지) +- **편차 한계**: clamp 폭·rate를 제거 목표량의 ±몇 %로? +- **(c) 에너지 루프 시상수 식별**: TICA-6111A 온도↔스팀 응답 — 데이터 식별 가능? + +### 7.6 리보일러 듀티 피드포워드 — TI-6103 + TICA-6111A.OP (2026-06-01 c) + +운전원 제안: 원료 프리히터 공급온도 **TI-6103**을 알 수 있고, 현재 평형에서 **TICA-6111A.OP**를 안다. TI-6103↔TICA-6111A ΔT를 감안해 OP가 얼마나 올라야 하는지 제안 가능. (**TICA-6111A.OP → FIQ-6115 스팀투입량** 결정) + +#### 에너지수지 +``` +Q_reboiler = 현열(피드 승온) + 잠열(증발) + = F·Cp·(T_b − T_feed[TI-6103]) + V·λ +필요 스팀 ≈ Q / λ_steam → FIQ-6115 target → OP +``` +- F(피드)·T_feed(TI-6103) = 피드포워드 입력, **둘 다 외란** +- **증분형이 강건**: ΔOP ≈ (dOP/dSteam)·ΔSteam, 현재 평형 OP 기준 + +#### 핵심 — §7.3 제약 (c)를 완화 +(c) 에너지 루프 lag가 binding이었던 이유 = TICA PID가 **피드백**(온도 droop 후 반응). 스팀 피드포워드(F·TI-6103로 OP/FIQ-6115 선제 구동)를 더하면 리보일러가 **선제 대응** → 온도 upset 없이 피드 램프 가능. **이 제안은 검증도구일 뿐 아니라 피드램프 가속 디커플러.** + +#### AUTO / MANUAL 활용 (운전원 통찰 — 맞다) +| PID 상태 | advisory 역할 | +|---|---| +| **AUTO** | advisory OP vs 실제 PID OP 비교 → **모델 검증**(정상상태에서만 valid). 일치=모델OK, 괴리=모델오차/미관측외란. **OP 쓰기 금지**(PID 소유), 표시만 | +| **MANUAL** | advisory OP = **운전원 의사결정 지원** ("이 피드엔 OP X로") | + +#### 보너스 — TI-6103 자체가 외란 피드포워드 +피드 일정해도 TI-6103↓(프리히터 파울링/상류 외란) → 현열부하↑ → 스팀 더 필요. advisory가 TICA 온도 droop **전에** 포착. 피드램프와 독립적 외란제거 가치. + +#### 일관성 — 또 같은 구조 +``` +OP_advisory = [에너지수지 base(FF)] + [PID/운전원 trim] +``` +B·D 드로우(§7.4)와 **동형**. 온 시스템이 "FF base + bounded 피드백 trim"으로 통일. + +#### 헛점 / 확인필요 +- **OP↔flow 비선형**: 절대 OP 계산은 밸브특성·스팀압 의존 → **flow 도메인(FIQ-6115 SP) advisory + 증분형**이 강건. **캐스케이드(TICA→FIQ flow loop)인지 OP직결인지 확인 필요** +- **AUTO 비교는 정상상태만**: 과도 중 PID OP는 동적/미분동작 포함 → 정상상태 비교만 valid +- **물성치**(Cp·λ·λ_steam)·**T_b**(압력·조성 의존, TICA PV 근사) 필요 +- **효율/열손실**: FF 게인의 미지 효율계수 → **AUTO 정상상태 OP vs 피드 회귀로 역산 보정** 가능 +- **안전**: 스팀 OP 직접쓰기 고위험 → AUTO=표시만, MANUAL=제안만, AdvisoryOnly 유지(D1) +- **상호작용**: 듀티가 선제적이 되면 드로우 trim과의 싸움(§6말미 헛점2) 오히려 **감소** + +### 7.7 결론적 산출물 — FIQ-6115 권장 스팀투입량 (운전원 deliverable) + +**최종적으로 운전원에게 줘야 하는 것 = "FIQ-6115를 얼마로 하라"** (스팀 flow 단위). OP가 아니라 flow로 주면 밸브 비선형(§7.6 헛점) 우회 + 운전원 즉시 실행 가능. + +#### 현재 평형 앵커링 — 물성치 없이도 강건 +현재 평형(F_cur, FIQ-6115_cur)을 신뢰(플랜트가 찾은 값) → 미지 상수(Cp·λ·효율) **소거**. +``` +FIQ-6115_target ≈ FIQ-6115_cur × [ 잠열분율·(F_new/F_cur) + + 현열분율·(F_new·(T_b−TI6103_new)) / (F_cur·(T_b−TI6103_cur)) ] + +1차 근사: FIQ-6115_target ≈ FIQ-6115_cur × (F_new/F_cur) + TI-6103 현열보정 +``` +- 잠열/현열 분율 모르면 → 피드비례(1차) + TI-6103 ΔT 보정으로 시작, **AUTO 정상상태 데이터로 분율 역산** 정밀화 +- 결과: **"피드 900→1100이면 FIQ-6115를 X→Y kg/hr로"** 운전원에게 직접 제시 +- "물성치 필요" 헛점(§7.6) 해소 — 절대계산 아닌 **현재 운전점 대비 증분** + +#### advisory 통합 산출물 (한 화면, 한 세트) +| 산출물 | 권장값 | 근거 | +|---|---|---| +| **FIQ-6115 (스팀)** | 목표 flow ("X→Y") | 에너지수지 앵커링 §7.6·7.7 | +| **B·D 드로우** | 목표제거량 + 편차 | 조성 §7.4 | +| **피드 램프율** | max RATE + 예상시간 | (a)밸브·(b)지연·(c)에너지 §3.1·7.3 | + +→ 피드를 올리되(램프율), 스팀을 **선제** 상향(FIQ-6115), 드로우를 조성목표로 맞춘다. 세 권장이 연동. + +### 7.8 캐스케이드 확인 결과 — OP 직결 (FIQ-6115 측정전용) 확정 (2026-06-01 d) + +**운전원 확인: TICA-6111A.OP는 스팀밸브 직결, FIQ-6115는 측정 전용**(스팀 flow 컨트롤러 없음). + +함의: +- 운전원이 직접 넣는 값은 **OP(%)**, FIQ-6115엔 SP가 없다 → "FIQ-6115 SP를 줘라"는 불가 +- §7.6 **OP↔flow 비선형 헛점 재활성** → 단, FIQ-6115 측정이 있어 해결 가능 + +#### 산출물 정정 (해결책) +- **1차 deliverable = FIQ-6115 목표 스팀 flow** (에너지수지 앵커링, 견고 — 밸브곡선 불요) +- **운전원 handle = OP(%)**. 두 가지 브리지: + 1. **운전원이 곧 flow 컨트롤러 (MANUAL)**: advisory가 flow 목표("X kg/hr까지")를 주고, 운전원이 OP를 올리며 **FIQ-6115 읽으면서** 도달. **밸브 모델 0 필요.** 가장 정직·견고. + 2. **시작 OP 제안 (보조)**: 라이브 OP↔FIQ-6115 회귀로 **local gain dFlow/dOP 온라인 추정** → `ΔOP ≈ ΔFlow / local_gain` → "OP X%→Y%부터" 제시해 도달 가속. 운전점 이동 시 재추정. + - **AUTO**: OP 자동. FIQ-6115 측정으로 **예측 flow vs 실측 검증** + local gain 역산. + +#### 부가 통찰 +스팀 공급압 변동 시 OP 고정이어도 flow 드리프트 → **FIQ-6115 측정이 그래서 필수 피드백**. "측정 전용"이 약점이 아니라 강점. + +#### 정정된 스팀 deliverable (§7.7 갱신) +| 산출물 | 운전원에게 | handle | +|---|---|---| +| 스팀 | **FIQ-6115 목표 flow "X→Y kg/hr"** | OP(%) 수동조작 + FIQ-6115 보며 도달 (+ 시작 OP 제안) | + +## 8. 편차(trim) 소스 정의 — 상세 브레인스토밍 (2026-06-01 e) + +### 8.1 편차의 역할 (재확인) +`B_SP = [조성분율×피드](base, 개루프 FF) + 편차_trim(피드백)`. base는 개루프라 오차 누적 → **편차가 측정 기반으로 보정**. 관건 = **무엇을 측정해 무엇을 목표로 driving 하나.** + +### 8.2 후보 소스 평가 +| 소스 | 타임스케일 | 장점 | 치명적 약점 | +|---|---|---|---| +| (a) 물질수지 vloss | 분~시간 | 질량보존 직접 | **스칼라 — 귀속 불가**(D/P/B·성분·계기바이어스 구분 못함) | +| (b) 조성분석(GC/랩) | 분(온라인)~시간(랩) | 직접·정확 | 온라인 없으면 느림, 랩은 trim용 불가 | +| (c) 온도·프론트(PCT) | 초~분 | 빠름·상시·**이미 구축**(WO-2/WO-5) | 간접 proxy, 압력·조성 민감, 섹션별 tray 필요 | + +### 8.3 핵심 문제 — vloss를 fast trim에 직접 넣으면 안 됨 +`vloss=F−(D+P+B)`는 스칼라. 중비물 축적인지 경비물 부족인지 구분 못함(상쇄 가능). **계기 바이어스일 수도** → 보정하면 공정을 계측오차에 맞춰 틀어버림(위험). → **vloss 단독 fast trim 금지.** + +### 8.4 제안 구조 — 2-타임스케일 + 프론트 귀속 +``` +B_SP = [중비물분율 × 피드] ← base (FF) + + fast_trim_B ← 하부 프론트/PCT error, bounded +base 분율 ← slow re-baseline ← vLossMa + 랩분석 (운전원 리뷰) +``` +1. **fast trim (초~분)**: 섹션별 sensitive-tray PCT/프론트 편차. **B=하부 프론트, D=상부 프론트**. 유계(clamp+rate) +2. **귀속 트릭**: vloss를 "**프론트 드리프트하는 스트림**"에 귀속 → 스칼라 모호성을 프론트가 해소(상부 고정·하부 상승=B 오차 국소화) +3. **slow re-baseline (시간)**: vLossMa+랩분석으로 base 분율 천천히 재보정. 자동 runaway 아닌 **운전원 리뷰** + +### 8.5 방향(sign) 로직 (calibrate 필요) +- **B**: 하부 PCT > ref (heavies 축적/프론트 상승, 제품 위협) → **B↑** / < ref (과추출, 수율손실) → **B↓** +- **D**: 상부 대칭 + +### 8.6 기존 자산 재사용 +PCT(WO-2)·sensitive tray·front indicator(WO-5)·vloss/vLossMa(WO-4)·transient gating 전부 이미 코드 존재. **편차 소스 = 이것들을 타임스케일 분리 + 유계로 배선.** + +### 8.7 이 구조의 헛점 +- **sensitive tray config 1개뿐** → 사이드드로우 2-프론트엔 **상/하 2개 필요**(B 하부·D 상부) +- **ref 온도 기준**: 평형 시드(PRef식)면 피드조성 변할 때 ref도 drift → 갱신 방법? +- **결합**: B·D trim + 듀티가 모두 온도 이동 → 상호작용. **trim rate ≤ 듀티 대역폭** 제약 +- **transient 중 온도는 조성 아닌 열동특성** → fast trim에 **transient 게이트 필수**(기존 재사용) +- **PCT 부정확**(DTdP 오보정) → proxy 붕괴 +- **편차 clamp 폭**: 좁으면 실드리프트 못잡고, 넓으면 듀티와 싸움 → 정하는 기준 미정 + +### 8.8 sensitive tray 1→2개 + tray 레이아웃 확정 (2026-06-01 h) + +#### 왜 2개인가 — 사이드드로우 = 3제품 = 2 front +제품 3개(D 경비물·**P 측류 주생성물 95%**·B 중비물) → 분리 front 2개. **sensitive tray 1개는 한 front만 보고 나머지 구간은 장님** → §8.4 "front 귀속" 트릭 작동 불가(B/D 독립 trim 불가). 제어공학 표준: 2-제품탑=온도1점, **3-제품(사이드드로우)탑=온도 2점**(dual/two-point temperature control). + +현재 `ColumnConfig.SensitiveTrayTag`는 **단일** + C-6111은 null이라 비활성. + +#### 확정된 전체 레이아웃 (P&ID 확인, 2026-06-01 j) — 패킹 3구간 +> **태그 번호 (확정)**: P&ID는 **10111**(TICA-10111A, TI-10111B/C/D), DCS/DB는 **6111**(tica-6111a, ti-6111b/c/d). **동일 계기 — 10111→6111 치환**(운전원 확인 2026-06-01). +``` +탑정 (pica-6111, 진공/경비물 D) +REFLUX DISTRIBUTOR (R ficq-6113 진입) +TI-6111D + PACKING (상부) — 정류, 제품 vs 경비물 D +제품추출노즐 P (ficq-6118) ← 전체높이 70~75% +TI-6111C (긴 패킹 상단 = 추출 직하) + PACKING (긴 구간) — 주 분리, 제품 vs 중비물 B ★ 핵심 +TI-6111B +원료 PREHEATER DISTRIBUTOR (피드 ficq-6101, TI-6103 온도) + PACKING (하부, 짧음) — stripping, 제품 회수 vs B +TICA-6111A(TE) — 컬럼 최하부/리보일러부, B 추출 (pi-6111b≈여기) +``` + +| 패킹 | 위치 | 역할 | bracket 온도 | +|---|---|---|---| +| 상부 | 제품~리플럭스 | 정류(경비물 D 제거) | TI-6111D ↔ TI-6111C | +| **중간(가장 김)** | **피드~제품** | **주 분리(중비물 B front)** | **TI-6111B ↔ TI-6111C** | +| 하부(짧음) | 리보일러~피드 | stripping(제품 탑저손실 방지) | TICA-6111A ↔ TI-6111B | + +- **이전 오류 수정**: 피드는 "리보일러 바로 위"가 아니라 **하부 짧은 패킹 위 PREHEATER DISTRIBUTOR**로 진입. **피드~제품 긴 패킹이 주 분리 구간** → B front가 최장 패킹에 걸쳐 **해상도 높음**. D 제거는 위쪽 짧은 정류. +- **TI-6111D = 리플럭스 직하** → R 즉답. R이 상부 front 조작변수라 위치 이상적 +- 두 front 모두 단일 단 아닌 **패킹 구배** → 민감도 충분 +- 제품·TI-6111C가 **70~75% 높이** → 위 25~30%만 정류(D), 아래 70%가 주분리+stripping + +#### ti-6111c 공유 기준 차온 + B 이중신호 +- **상부 front (D)** = `ΔT(TI-6111C − TI-6111D)` (상부 정류 패킹): 경비물(저비점) 제품쪽 하강 시 구배 변화 → 침투 → **D↑**. **⚠ §10.2-B: TI-6111D는 환류 서브쿨링 오염 → 이 정의 재검토 필요(D 보정/대체)** +- **하부 front (B)** = `ΔT(TI-6111C − TI-6111B)` (긴 중간 주분리 패킹): 중비물(고비점) 제품쪽 상승 시 침투 → **B↑** +- **(보너스) stripping** = `ΔT(TI-6111B − TICA-6111A)` (하부 짧은 패킹): 제품(저비점)이 탑저로 손실되면 구배 변화 → **제품 탑저손실/B 과추출 조기경보** → B는 신호 2개(중간=중비물 침투 / 하부=제품 손실) +- ti-6111c 공유 pivot → 차온 자연 분리 → §8.4 귀속 작동 + +#### 보너스 — 제어 위계 명확화 +- **ti-6111c(PCT) = 제품 품질 proxy = 주 제어변수**(제품 spec). reflux(R)/duty로 1차 +- 상/하 front(D·B trim) = spec 유지 받치는 2차 보조 + +#### 보너스 — 압력 프로파일 보간 (§9 연결) +압력 2점(pica-6111 탑정, pi-6111b≈리보일러) → 단높이 선형보간으로 각 온도에 국소압 부여: +ti-6111d≈pica-6111 / tica-6111a≈pi-6111b / **ti-6111c·ti-6111b=보간**. → §9 PCT를 단일압이 아닌 **프로파일 기반**으로. ti-6111c 제품온도 보정 정확도가 핵심. + +#### 코드/모델 변경 +| 현재 | 필요 | +|---|---| +| `SensitiveTrayTag` (단일) | `UpperSensitiveTrayTag`+`LowerSensitiveTrayTag` (또는 섹션별 차온쌍) | +| `st.FrontInd` 1개 | 상/하 2개 인스턴스 | +| `ApplyFront`→단일 (state,trim) | 섹션별 반환 | +| §8 귀속 | 상부→D trim, 하부→B trim | + +#### sub-헛점 / 확인필요 +- **two-point 루프 간섭**: D·B 완전독립 아님(reflux/duty·내부 traffic 공유). 디커플링 또는 한쪽 detune 필요할 수 있음(RGA 영역) +- **tray 선정**: 각 구간 최민감 지점은 운전 프로파일로 확인(고정가정 위험) +- ~~태그 10111 vs 6111~~ → **확정**: 동일계기, P&ID 10111 → DCS 6111 치환 +- ~~ti-6111b 위치·피드 진입단·ti-6111c~d 중간단~~ → **해소**(2026-06-01 j): 전체 레이아웃 확정. 피드=하부 짧은패킹 위 PREHEATER DIST, ti-6111b=피드 직상, ti-6111c~d 사이=상부 정류 패킹 + +## 9. 진공압 프로파일 — pi-6111b 미사용 + ΔP/PCT 함의 (2026-06-01 f) + +### 9.1 현재 실태 (DB 확인, ff_column_config id=1) +| 항목 | 값 | 의미 | +|---|---|---| +| `pressure_tag` | **pica-6111만** | **pi-6111b 미사용** | +| `dtdp` | **0.0** | **PCT(압력보정온도) 꺼짐** — pct=raw | +| `delta_p_tag` | **null** | ΔP 플러딩 트리거 비활성 | +| `sensitive_tray_tag` | **null** | 프론트/θ 기능 비활성 | +| `p_ref` | null | 첫 압력 시드 | +| `temp_tags` | tica-6111a, ti-6111b, ti-6111c, ti-6111d | 수집됨 | + +- pi-6111b는 **OPC 태그로 수집은 됨**(realtime/history DB 존재) — FF 엔진이 안 읽을 뿐 +- pica-6111조차 현재 `pUnstable`(과도감지)용뿐 + `PressureBand` 기본 무한대 → 사실상 거의 비작동 +- **즉 압력/PCT/ΔP/sensitive-tray 서브시스템이 통째로 dormant** + +### 9.2 압력 프로파일 의미 +진공탑 → 아래로 갈수록 절대압↑. **pica-6111(최상단) < pi-6111b(≈ 리보일러 위, 피드 디스트리뷰터 하단)**. + +**운전원 정정 (2026-06-01 g): pi-6111b ≈ 리보일러 압력**으로 간주. → 탑저 압력이 실측되는 셈. +``` +컬럼 ΔP(전탑, top→bottom) = pi-6111b − pica-6111 (> 0) +``` +함의: **피드가 탑 하부 진입**(디스트리뷰터 하단=리보일러 위) → §7.6 에너지 노드가 한 점에 모임(피드 TI-6103 진입 → pi-6111b 비점까지 승온 → 직하 리보일러 듀티). + +### 9.3 pi-6111b가 우리 설계의 빠진 조각인 이유 +| 설계 항목 | pi-6111b 기여 | +|---|---| +| **§3.2 플러딩 ceiling** | 별도 ΔP 트랜스미터 없이 **pi-6111b − pica-6111 = ΔP** → flooding 지표. `delta_p_tag`을 이 식/파생태그로 채움 | +| **§7.6/7.7 에너지수지 T_b** | 비점=국소 압력 함수. pi-6111b≈리보일러압이므로 **탑저 비점 = pi-6111b 비점, tica-6111a가 그 실측치** → 추가 ΔP 보정 불요 + **tica-6111a ≈ 탑저조성 비점@pi-6111b** 교차검증 | +| **§8 편차 trim PCT proxy** | §8.7 "PCT 부정확→proxy 붕괴" 헛점의 근원. dtdp=0이라 PCT 꺼짐 + 단일 탑정압으론 하부(B 귀속) 보정 부정확. **pi-6111b가 하부 섹션 PCT의 기준압** → §8 B fast-trim 작동 전제 | + +### 9.4 제안 배선 +- `pi-6111b` 수집값 → **ΔP 파생** = pi-6111b − pica-6111 → `delta_p_tag`(또는 계산 컬럼)로 플러딩 ceiling 공급 +- **단별 기준압 = 패킹가중 보간**(높이선형 아님, 긴 중간 패킹이 ΔP 대부분): pica-6111(탑정)~pi-6111b(탑저) 사이를 패킹구간/traffic로 분배. ti-6111d≈pica-6111+상부패킹ΔP / **제품·ti-6111c(70~75%높이)≈pica-6111+상부패킹ΔP** / ti-6111b≈+상부+중간패킹ΔP / tica-6111a≈pi-6111b. **제품온도(ti-6111c) PCT 기준압 = 제품 품질추정 직결** +- `dtdp` 실측 보정계수로 설정(현재 0=꺼짐) → PCT 활성화 +- → §8 편차 trim(특히 B 하부)이 비로소 유효해짐 (의존성) + +### 9.5 헛점/확인필요 +- ~~pi-6111b는 중하부라 탑저는 더 아래 미계측~~ → **해소** (2026-06-01 g): pi-6111b ≈ 리보일러압. 탑저 압력 실측되는 셈, 추가 ΔP 보정 불요 +- ΔP = pi-6111b − pica-6111 의 부호·스팬 실측 확인 (둘 다 진공, mmHg). 이제 **전탑 ΔP** +- 진공계 신선도/stall 게이트 (압력도 corroboration 대상) +- tica-6111a(리보일러 온도) = 탑저 비점@pi-6111b 교차검증 활용 — 괴리 시 조성변화/계기이상 신호 + +## 10. 온도 프로파일 단조성 · 역전 판정 · 프론트 부호 (2026-06-01 k) + +### 10.1 확정 baseline (운전원) +정상 생산 시 **A>B>C>D 단조 감소** (탑저 tica-6111a 최고온 → 탑정 ti-6111d 최저온). A−D ≈ 10°C — 단 그 상당분은 **D의 환류 서브쿨링**(리플럭스 디스트리뷰터 냉액)에서 옴(조성 아님). + +### 10.2 발견된 버그/문제 (코드 확인) +| # | 문제 | 영향 | 등급 | +|---|------|------|------| +| A | **부호 역전 버그** — `DiffTemp.Delta(tHi,tLo)=tHi−tLo` 주석="상단−하단", 그러나 `ApplyFront`(engine.cs:255) 호출=`Delta(temps[0]=A=하단, temps[^1]=D=상단)`=A−D=**하단−상단** → 규약 반대부호 | FrontPositionIndicator `dev>0→프론트상승→환류↑` 매핑이 "상단−하단" 가정 → **트림 권고 반전 + ApplyRecovery sigFront 오판** | 🔴 | +| B | **D 환류 서브쿨링 오염** — ti-6111d=리플럭스 직하라 조성 아닌 환류온도 추종 | A−D·ΔT(C−D)가 분리 아닌 환류에 끌림 → §8.8 상부 front(D 사용) 약화 | 🔴 | +| C | **역전 가드 부재** — 단조성 검증 없음, 순서 가정 silent | 실제 이상역전(플러딩 등) 미검출 | 🟡 | + +### 10.3 역전(inversion) 판정 — 구체 spec (설계, 미구현) + +#### 입력 +- **신뢰 구간 = A,B,C** (조성 트레이). C-6111: A=`tica-6111a`, B=`ti-6111b`, C=`ti-6111c`. **D(`ti-6111d`)는 환류 서브쿨링 오염이라 단조 체인에서 제외**(§10.2-B). +- 온도는 **PCT 적용값** 사용(§9 압력 프로파일 보간 의존). PCT 미활성(dtdp=0)이면 raw + warning="PCT off, 역전판정 정확도 저하". +- **게이팅**: `transient`(피드 이동·압력 불안정·정착대기) 중에는 판정 보류(열동특성 노이즈). 모든 입력 `Good`(신선) 필수, stale 시 보류. + +#### 판정량 +인접쌍 하강폭(정상 양수): +``` +ΔAB = T(A) − T(B) , ΔBC = T(B) − T(C) // 정상 둘 다 > 0 +span = T(A) − T(C) // 정상 양수 (분리 강도 지표) +``` +기준값 `spanRef`: cfg 지정 또는 **최초 정상상태에서 시드**(PRef 방식). + +#### 상태 분류 (우선순위 순) +| 상태 | 조건 | 의미 | 등급 | +|---|---|---|---| +| **온도역전** | 임의 인접쌍 `ΔAB < −tolInv` 또는 `ΔBC < −tolInv` (상단이 하단보다 더 뜨거움) | 조성 upset / 플러딩 / 센서이상 | 🔴 | +| **프로파일 붕괴** | `span < spanCollapseFrac × spanRef` | 분리 손실(평탄화) | 🟠 | +| **약화** | `span < spanWarnFrac × spanRef` (붕괴 전 단계) | 분리 여유 감소 | 🟡 | +| **정상** | 위 해당 없음 | — | — | + +#### 파라미터 (기본값, 운전 데이터로 튜닝) +- `tolInv` = 0.5 °C (센서 노이즈 + 여유; 이 이상 음수라야 역전 인정) +- `spanWarnFrac` = 0.5, `spanCollapseFrac` = 0.3 (spanRef 대비) +- `spanRef` 시드: 최초 비-transient·전부 Good 인 tick의 span + +#### 조치 +- **front advice**: 역전/붕괴 시 front trim **HOLD** + confidence 강등(역전 중 front 방향 신뢰 불가). +- **ApplyRecovery 연동**: 새 severity 신호 `sigInv`(역전) / `sigCollapse`(붕괴)를 기존 `sigVloss·sigFront·sigDp`와 **병렬 OR**로 추가. (advisory·ARM 게이트 유지 — 자동쓰기 아님.) +- **센서 vs 공정 구분(corroboration)**: 역전이 ΔP(`pi-6111b−pica-6111`)·vloss와 **동반·지속**이면 공정(플러딩/조성); 단발·점프성이면 **센서이상 의심** → 역전 신호 억제 + 계기점검 권고. (단독 온도로 공정 단정 금지.) +- **D 특수처리**: C−D 쌍은 서브쿨 baseline이라 절대 단조검증서 제외. 필요시 **서브쿨 보정 후 deviation**으로만 별도 모니터(역전 트리거엔 미포함). + +#### 출력 (advisory 필드 추가 제안) +`tempProfileState`(정상/약화/붕괴/역전) + `inversionPair`(예 "B-C") + `span`/`spanRef`. + +#### 구현 메모 +- 신규 순수함수 `TempProfileJudge.Evaluate(temps[A,B,C], spanRef, params)` → 단위테스트(정상/역전/붕괴 각 케이스). `FeedforwardEngine`은 호출만(엔진 로직 최소 변경). +- §10.2-A(부호) 버그픽스와 독립. WP2 P6에서 정상 프로파일·역전 재현으로 검증. + +### 10.4 제안 — 프론트 부호 정정 +- metric을 명시 "상단−하단"으로: `Delta(temps[^1], temps[0])`(D−A) 또는 부호 반전 → FrontPositionIndicator 매핑과 정합. +- 단 D 오염 때문에 상부 front는 D 대신: (b1) D를 환류온도 기준 보정, (b2) C 단독 PCT 변화, (b3) D=에너지/서브쿨용 재지정 + 경비물 front는 탑정 조성/분석. +- 하부 front(B)는 A,B,C만 쓰므로 부호 정정 후 신뢰 가능. + +### 10.5 작업 항목 +- (버그픽스, 엔진수정이라 별도) DiffTemp 호출 부호 정정 + 단위테스트. +- (WP2 프로브) 정상시 A>B>C>D 확인, A−D≈10 중 환류 서브쿨 기여 측정, 부호 역전 재현. +- (§8.8 갱신) 상부 front 센서 D 의존 재검토(b1/b2/b3). + +## 11. 참고 + +- `docs/측류추출식-통합유량설정공식.md` (§9~§12 advisory 엔진) +- `src/Infrastructure/Control/FeedforwardEngine.cs` (ComputeStream, deadtime/lag/rate) +- `src/Core/Application/Feedforward/FeedforwardModels.cs` (StreamConfig) +- `docs/운전원교육-FF과도상태-메커니즘.md` diff --git a/plans/안전피드램프-advisory-작업지시서.md b/plans/안전피드램프-advisory-작업지시서.md new file mode 100644 index 0000000..c2eaa78 --- /dev/null +++ b/plans/안전피드램프-advisory-작업지시서.md @@ -0,0 +1,364 @@ +# 작업지시서 — 안전 피드램프 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건**. + +### 실행자 사용 예 (자율 루프) +``` +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`