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); + } }