fix: Feedforward 버그 2건 — config ordinal off-by-one + front 부호 반전

- 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 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-06-01 15:28:21 +09:00
parent 7c26aa7361
commit 76fdce8b13
3 changed files with 57 additions and 2 deletions

View File

@@ -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<TagSample>(),
new Dictionary<string, TagSample> { ["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);
}
}