From 76fdce8b135eaf0165b359a6a7d74401d10e2c59 Mon Sep 17 00:00:00 2001 From: windpacer Date: Mon, 1 Jun 2026 15:28:21 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Feedforward=20=EB=B2=84=EA=B7=B8=202?= =?UTF-8?q?=EA=B1=B4=20=E2=80=94=20config=20ordinal=20off-by-one=20+=20fro?= =?UTF-8?q?nt=20=EB=B6=80=ED=98=B8=20=EB=B0=98=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FeedforwardConfigStore: advisory_only를 GetBoolean(31)로 읽어 IndexOutOfRange (컬럼 31개=ordinal 0~30, advisory_only=30). 30으로 수정 → FF supervisor 루프 복구 - FeedforwardEngine.ApplyFront: front metric을 Delta(temps[0],temps[^1])=하단−상단으로 계산해 부호 반전(프론트 상승 시 trim 권고 역전). Delta(temps[^1],temps[0])=상단−하단으로 수정 - FeedforwardFrontTests: 엔진 경유 부호 회귀 테스트 2건 추가 (24/24 통과) Co-Authored-By: Claude Opus 4.8 --- .../Control/FeedforwardConfigStore.cs | 2 +- .../Control/FeedforwardEngine.cs | 7 ++- .../FeedforwardFrontTests.cs | 50 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/Control/FeedforwardConfigStore.cs b/src/Infrastructure/Control/FeedforwardConfigStore.cs index 6dff65f..7743d96 100644 --- a/src/Infrastructure/Control/FeedforwardConfigStore.cs +++ b/src/Infrastructure/Control/FeedforwardConfigStore.cs @@ -57,7 +57,7 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore Id = rd.GetInt32(0), Name = rd.GetString(1), Enabled = rd.GetBoolean(2), - AdvisoryOnly = rd.GetBoolean(31), + AdvisoryOnly = rd.GetBoolean(30), FeedTag = rd.GetString(3).ToLowerInvariant(), PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4).ToLowerInvariant(), LevelTags = levelTags, diff --git a/src/Infrastructure/Control/FeedforwardEngine.cs b/src/Infrastructure/Control/FeedforwardEngine.cs index 578589b..79b244f 100644 --- a/src/Infrastructure/Control/FeedforwardEngine.cs +++ b/src/Infrastructure/Control/FeedforwardEngine.cs @@ -252,7 +252,12 @@ public sealed class FeedforwardEngine } if (double.IsNaN(metric) && temps.Count >= 2 && temps[0].Good && temps[^1].Good) { - metric = DiffTemp.Delta(temps[0].Pct, temps[^1].Pct); + // temp_tags는 하단→상단 순서(예: tica-6111a..ti-6111d, 정상 A>B>C>D 단조감소). + // front metric = "상단−하단"(top−bottom): 프론트 상승(heavies 상승→상단 가열) 시 증가 → + // FrontPositionIndicator의 dev>0→"상승"→환류↑ 규약과 정합. + // [버그픽스] 이전 Delta(temps[0],temps[^1])=하단−상단은 부호 반대라 트림권고가 반전됐음(브레인스토밍 §10.2-A). + // ⚠ §10.2-B: temps[^1](ti-6111d)는 환류 서브쿨링 오염 가능 — 센서 선정/보정 재검토는 후속. + metric = DiffTemp.Delta(temps[^1].Pct, temps[0].Pct); strong = true; } if (double.IsNaN(metric)) return (null, null); diff --git a/tests/ExperionCrawler.Tests/FeedforwardFrontTests.cs b/tests/ExperionCrawler.Tests/FeedforwardFrontTests.cs index 54af80a..522eccd 100644 --- a/tests/ExperionCrawler.Tests/FeedforwardFrontTests.cs +++ b/tests/ExperionCrawler.Tests/FeedforwardFrontTests.cs @@ -37,4 +37,54 @@ public class FeedforwardFrontTests Assert.Contains("하강", state); Assert.Contains("boilup", trim); } + + // ── ApplyFront 부호 규약 (브레인스토밍 §10.2-A 버그픽스 회귀) ────────── + // temp_tags = [A하단 … D상단], 정상 A>B>C>D 단조감소. + // front metric은 "상단−하단"이어야 함: 프론트 상승(heavies↑→상단 가열) 시 metric↑ → dev>0 → 상승/환류↑. + private static ColumnConfig FrontCfg() => new() + { + Id = 1, Name = "C-FRONT", Enabled = true, FeedTag = "f", ProductKey = "P", ScanSec = 2, + DTdP = 0.0, PRef = double.NaN, PressureTag = null, // 압력경로 비활성 → pUnstable 없음 + FeedMoveThresholdPerMin = 0.0, // moving 비활성 → transient 없음 + TempTags = new[] { "a", "b", "c", "d" }, + Streams = new[] { new StreamConfig { Key = "P", FlowTag = "ficq-6118", Role = StreamRole.Commanded, Grade = Confidence.A, TargetCoeff = 0.95 } } + }; + + private static PvSnapshot FrontSnap(double a, double b, double c, double d) => new( + new TagSample("f", 100, true, DateTime.UtcNow), + null, + Array.Empty(), + new Dictionary { ["P"] = new TagSample("ficq-6118", 95, true, DateTime.UtcNow) }) + { + Temps = new[] + { + new TagSample("a", a, true, DateTime.UtcNow), new TagSample("b", b, true, DateTime.UtcNow), + new TagSample("c", c, true, DateTime.UtcNow), new TagSample("d", d, true, DateTime.UtcNow) + } + }; + + [Fact] + public void ApplyFront_top_warming_is_front_rise_reflux_advice() + { + var engine = new FeedforwardEngine(); + var st = new ColumnState(); + // 정상 단조감소(A=110>B>C>D=90)로 baseline 시드 + for (int i = 0; i < 5; i++) engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 90), st, DateTime.UtcNow); + // 상단(D) 가열 = heavies 상승 = 프론트 상승 → 상승/환류↑ (버그였다면 하강/boilup로 반전) + var res = engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 100), st, DateTime.UtcNow); + Assert.Contains("상승", res.FrontPositionState); + Assert.Equal("환류↑ 권장", res.FrontTrimAdvice); + } + + [Fact] + public void ApplyFront_top_cooling_is_front_fall_boilup_advice() + { + var engine = new FeedforwardEngine(); + var st = new ColumnState(); + for (int i = 0; i < 5; i++) engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 90), st, DateTime.UtcNow); + // 상단(D) 추가 냉각 = 프론트 하강 → 하강/boilup + var res = engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 80), st, DateTime.UtcNow); + Assert.Contains("하강", res.FrontPositionState); + Assert.Contains("boilup", res.FrontTrimAdvice); + } }