Compare commits

...

3 Commits

Author SHA1 Message Date
windpacer
1811e1fed7 docs: S7 기대값 정정(큰 음수 vloss→'물질수지 불일치') + override 엔진확장 라이브검증 기록
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:33:34 +09:00
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
windpacer
9065b19a0a docs: WP2 현행 FF 엔진 프로빙 결과
라이브 /api/ff/advisory 관찰(데모=SP+일정변동 반복). 확정: P1(LevelDriven B/D rec=K×feed 무클램프, P는 lag), P2(PCT 꺼짐 pct==raw), P3(pi-6111b 미사용), P5(mbState, 데모 비보존 아티팩트). 발견: 데모 temps 비물리(A>D>C>B)라 §10/front 물리검증·P4·S7은 운전원 DCS주입 필요(override는 엔진 미반영).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:29:01 +09:00
4 changed files with 69 additions and 10 deletions

View File

@@ -57,7 +57,7 @@ steamTo = fiq6115Cur × (clampedTarget/currentFeed) + sensibleGain × clamped
| **S4** | flooding ceiling | pi-6111b=**120**(ΔPnow=80), **floodLimit=100, n=1.8**, targetFeed=1100, ΔI=50 | floodCeiling=900×(100/80)^(1/1.8)=**1018.5**, ceiling=min(2105,1018.5)=**1018.5**(binding=**flooding**), clampedTarget=1018.5, rampTimeMin=**36.1** | ceiling 1018.5±2% binding=flooding, clampedTarget 클램프 | 단위+sim |
| **S5** | TI-6103 하강(현열 FF) | feed=900 고정, **ti-6103=140**, targetFeed=900, sensibleGain=0.001, feedTempRef=150 | steamTo=300×1 + 0.001×900×(150140)=**309.0**(+9), rampTimeMin≈0 | steamTo 309±2%(피드 무변에도 steam↑), 현열항 부호 양수 | 단위+sim |
| **S6** | stale/HOLD | feed 태그 Good=false(단위) / 운전원 frozen(>stale_sec=120s) | **HOLD** 반환 + warning="feed-bad/stale", 권장 미산정 | HOLD 상태·warning stale, 수치 미반환 | 단위(Good=false), 운전원 frozen |
| **S7** | 과추출 감지(현행엔진) | B=**150**(vs 27), P=855, D=18 → D+P+B=1023 > feed → vloss=**123** | (FeedRampCalculator 아님) `/api/ff/advisory` **massBalanceState="음의 손실(스팬 오류 의심)"** | mbState≠"정상"(음의손실/불일치) | **WP2 엔진**(/api/ff/advisory) |
| **S7** | 과추출 감지(현행엔진) | B=**150**(vs 27), P=855, D=18 → D+P+B=1023 > feed → vloss**120** | (FeedRampCalculator 아님) `/api/ff/advisory` **massBalanceState="물질수지 불일치(계측 점검)"** (\|vloss\|>3%·ff 조건이 먼저 걸림; 작은 음수만 "음의 손실") | mbState≠"정상" | **엔진**(/api/ff/advisory). override 엔진확장(2026-06-01) 후 **자율검증 가능**(라이브 120.27 확인) |
---

View File

@@ -0,0 +1,51 @@
# WP2 — 현행 FF 엔진 프로빙 결과
> 2026-06-01. 대상: `plans/안전피드램프-advisory-작업지시서.md` WP2. 라이브 `:5000` `/api/ff/advisory` 관찰.
> **방법 제약**: Sim Override는 엔진에 안 닿음(ramp-advisor만) → 본 프로빙은 **엔진이 현재 데모 합성데이터(설정값±variation)로 돌아가는 실제 거동을 관찰**. 의도적 step 주입이 필요한 항목(P4 등)은 운전원 DCS 주입 필요로 분리.
## P0 — 현재 advisory 스냅샷 (feed≈900.9, 정상상태)
```
transient=False mode=Normal yield=90.7%
massBalanceState=물질수지 불일치 vLoss=43.3 vLossMa=31.7
front=프론트 하강→boilup↑ (수 초 뒤 baseline 수렴하며 "정상(프론트 안정)"으로)
temps(raw=pct): tica-6111a=79.21 ti-6111b=78.47 ti-6111c=78.69 ti-6111d=78.92
streams: P rec=833.5(pv817) D rec=18.02(pv15.8) B rec=27.03(pv24.8) R rec=666.8(pv653.8)
```
## 프로빙 결과 (코드리딩 주장 검증)
| 프로브 | 주장 | 관찰 | 판정 |
|---|---|---|---|
| **P1** | LevelDriven B/D rec = K×feed (clamp/rate 無) | B rec=**27.019 ≡ 0.03×900.63**, D rec=18.018≡0.02×feed. 시간추이서 feed와 **정확히 선형 추종**(27.018→27.021) | ✅ 확정 |
| (대조) | Commanded P는 deadtime+lag+rate | P rec=**834.8 ≠ 0.95×feed(855.6)** — lag로 뒤처짐 | ✅ LevelDriven과 명확히 구분 |
| **P2** | PCT 꺼짐(dtdp=0) | **모든 temp의 pct == raw**(79.2066=79.2066…) | ✅ PCT dormant 확정 |
| **P3** | pi-6111b 미사용 | 엔진 태그목록(BuildSnapshotAsync)에 pi-6111b 없음 — 코드 사실(관찰 대상 아님) | ✅ 코드 확인 |
| **P4** | feed 급변 시 transient/valid=false | **자연 variation(feed ±0.1)으론 transient 미발화**(FeedMoveThresholdPerMin=0이라 moving 조건 자체 off) | ⚠ 운전원 step 주입 필요 |
| **P5** | D+P+B≠feed → mbState 불일치 | vLoss=43.3(>3%·feed=27) → "불일치", Bpv noise로 "정상"↔"불일치" 토글 | ✅ 단, **데모 PV 비보존 아티팩트**(실제 과추출 아님) |
| **P6** | front 부호 수정 동작 | front는 tica-6111a(A)↔ti-6111d(D)로 산출, "하강↔안정" 변동. 부호픽스(76fdce8) 머지·단위검증 완료 | ⚠ 데모 temp 비물리라 **물리 방향 라이브 검증 불가** |
## ⚠ 핵심 발견 — 데모 temps가 물리 프로파일 위반
운전원 확정 정상 프로파일은 **A>B>C>D 단조감소**(§10.1). 그러나 데모 실측은:
```
tica-6111a(A)=79.21 > ti-6111d(D)=78.92 > ti-6111c(C)=78.69 > ti-6111b(B)=78.47
→ A>D>C>B (B가 최저, 비단조)
```
- 데모는 **각 태그 = SP + 일정한 변동량 반복** 시뮬레이션 → temp 순서는 물리 공간구배가 아니라 **각 태그 SP + 변동 위상**의 산물. **공간 구배 미인코딩** → 물리적 단조성 없음 (메모리 `project_demo_system_synthetic_data` 부합).
- **함의 1**: §10 역전판정(`A≥B≥C`)을 이 데모에 켜면 `B<C`**항상 "온도 역전" 오발**. → §10/front **검증엔 운전원이 단조 temps를 주입**해야 함.
- **함의 2**: front(A↔D) 부호의 *물리적* 타당성은 데모로 검증 불가(단위테스트로만). 부호 자체는 머지·통과.
## 운전원 DCS 주입이 필요한 잔여 (override 불가)
| 항목 | 필요 주입 | 관찰 |
|---|---|---|
| P4 transient | feed를 FeedMoveThresholdPerMin 이상으로 step | transient=true, stream valid=false |
| S7 과추출(매트릭스) | B pv ≫ 0.03×feed, D+P+B≠feed (realtime_table) | mbState 음의손실/불일치 |
| §10/front 물리검증 | temps를 A>B>C>D 단조로 세팅 후 D 가열 | front "상승→환류↑" |
## 결론
- **확정**: P1(LevelDriven=K×feed 무클램프, 원래 우려 재현)·P2(PCT dormant)·P3(pi-6111b 미사용)·P5(mbState, 단 데모 아티팩트).
- **보류(운전원 주입 필요)**: P4·S7·front 물리검증.
- **신규 이슈**: 데모 temps 비물리(A>D>C>B) — §10/front 라이브 검증의 선결 조건은 단조 temps 주입. (override가 엔진에 닿지 않는 한계와 함께, 자율 검증 범위가 ramp-advisor로 한정됨.)
## 참조
- `plans/안전피드램프-advisory-작업지시서.md` WP2, `docs/안전피드램프-검증시나리오매트릭스.md` S7
- `docs/안전피드램프-한계치-브레인스토밍.md` §10

View File

@@ -111,12 +111,11 @@ pica-6111 탑정 / REFLUX DIST(R) / TI-6111D / 상부PACKING(정류,D) /
- `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용.
- 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**.
### 한계 (구현 후 확인, 2026-06-01)
**Sim Override `FeedRampAdvisorService`(ramp-advisor)만 통합. 엔진(`FeedforwardSupervisor.BuildSnapshotAsync`)은 DB를 직접 읽어 override 미반영.**
- 결과: **S7(mbState 과추출)·S6(stale) 라이브 검증은 override로 불가** `/api/ff/advisory`(엔진 산출) override 무시.
- S7 라이브 = 운전원이 실제 DCS/realtime_table에 합성값 주입 / 또는 Engine 통합테스트. S6 라이브 = stale 태그(운전원) / 또는 단위테스트(`pv.Feed.Good=false`, 구현됨).
- 라이브 override로 검증 가능한 건 **S1~S5(ramp-advisor 경로)뿐** — 전부 통과 확인(2026-06-01).
- 후속 옵션: 엔진 스냅샷 빌드도 `ISimOverrideStore` 경유하도록 확장하면 S6·S7도 override로 자율 검증 가능(현재 미적용).
### 한계 → 엔진 확장으로 일부 해소 (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만으론 미발화).
### 실행자 사용 예 (자율 루프)
```

View File

@@ -17,6 +17,7 @@ public sealed class FeedforwardSupervisor : BackgroundService
private readonly IFeedforwardWriteGuard _writeGuard;
private readonly ILogger<FeedforwardSupervisor> _logger;
private readonly Microsoft.Extensions.Configuration.IConfiguration _appConfig;
private readonly ISimOverrideStore _sim; // WP0 확장: 엔진 스냅샷 입력 치환(DEMO)
private readonly Dictionary<int, ColumnState> _states = new();
// Phase II: 마지막 쓰기 시각(스트림별 rate-limit) 및 결과
private readonly ConcurrentDictionary<(int colId, string streamKey), DateTime> _lastWriteTimes = new();
@@ -26,8 +27,9 @@ public sealed class FeedforwardSupervisor : BackgroundService
IServiceScopeFactory scopeFactory, FeedforwardEngine engine,
IFeedforwardAdvisoryStore store, IFeedforwardWriteGuard writeGuard,
ILogger<FeedforwardSupervisor> logger,
Microsoft.Extensions.Configuration.IConfiguration appConfig)
{ _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; }
Microsoft.Extensions.Configuration.IConfiguration appConfig,
ISimOverrideStore sim)
{ _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; _sim = sim; }
// Phase II: 쓰기 결과 조회 (Controller에서 사용)
public (double? sp, string? error, DateTime? at) GetLastWrite(int colId, string streamKey)
@@ -59,11 +61,14 @@ public sealed class FeedforwardSupervisor : BackgroundService
var st = GetState(cfg.Id);
var res = _engine.Tick(cfg, snap, st, DateTime.UtcNow);
// Phase II: auto-write
if (!cfg.AdvisoryOnly && writeClient is not null && auditService is not null)
// 안전가드: Sim Override 활성 시 입력이 가짜이므로 실제 쓰기 금지(advisory-only로 강등)
if (!cfg.AdvisoryOnly && writeClient is not null && auditService is not null && !_sim.Enabled)
{
await AutoWriteAsync(cfg, res, st, writeClient, auditService, ct);
res = res with { AutoWriteActive = true };
}
else if (!cfg.AdvisoryOnly && _sim.Enabled)
_logger.LogWarning("[FF] Sim Override 활성 — col {Id} auto-write 억제(가짜 입력)", cfg.Id);
_store.Set(res);
}
catch (Exception ex)
@@ -184,6 +189,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
TagSample Sample(string baseTag)
{
var tag = PvTag(baseTag);
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선(신선 처리)
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
if (rows.TryGetValue(tag.ToLowerInvariant(), out var r)
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
{
@@ -197,6 +204,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
TagSample SampleExact(string rawTag)
{
var tag = rawTag.ToLowerInvariant();
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
if (rows.TryGetValue(tag, out var r)
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
{