feat: WP4-7 안전피드램프 후속 — 역전코러보·ΔP합성·국소PCT·프론트UI

This commit is contained in:
windpacer
2026-06-01 17:29:38 +09:00
parent 25fd969276
commit 89187aff19
8 changed files with 438 additions and 14 deletions

View File

@@ -84,4 +84,99 @@ public class FeedforwardRecoveryTests
var res = engine.Tick(Cfg(autoArm:true), Imbalanced(), st, DateTime.UtcNow);
Assert.Equal(ColumnMode.Normal, res.Mode);
}
// ── WP4: 온도역전 + vloss 코러보레이션 ────────────────────────────
// temp_tags 하단→상단: 정상 단조감소(110>105>100>90). 역전은 하단이 상단보다 차가워야 하므로
// 하단(110) > 중간(100) > 상단(105 → 역전: 100-105 < -tolInv)
// 열순서: A(tica-6111a)=리보일러→B(ti-6111b)=중간→C(ti-6111c)=제품→D(ti-6111d,제외)
// 조성트레이(A,B,C) 역전: 태그 D(ti-6111d)는 제외되므로, B(ti-6111b)가 C(ti-6111c)보다
// 차가우면 역전(B-C): B=95, C=100 → 95-100=-5 < -0.5 → 온도역전
private static PvSnapshot ImbalancedWithInversion() => new(
new TagSample("f", 100, true, DateTime.UtcNow), null, Array.Empty<TagSample>(),
new Dictionary<string, TagSample> {
["P"]=new("p",30,true,DateTime.UtcNow), ["R"]=new("r",50,true,DateTime.UtcNow),
["D"]=new("d",2,true,DateTime.UtcNow), ["B"]=new("b",3,true,DateTime.UtcNow) })
{ Temps = new[] {
new TagSample("tica-6111a", 110, true, DateTime.UtcNow), // 하단 A
new TagSample("ti-6111b", 95, true, DateTime.UtcNow), // B (C보다 차가움 → B-C 역전)
new TagSample("ti-6111c", 100, true, DateTime.UtcNow), // C
new TagSample("ti-6111d", 90, true, DateTime.UtcNow) }};// D(제외)
private static PvSnapshot BalancedWithInversion() => new(
new TagSample("f", 100, true, DateTime.UtcNow), null, Array.Empty<TagSample>(),
new Dictionary<string, TagSample> {
["P"]=new("p",95,true,DateTime.UtcNow), ["R"]=new("r",760,true,DateTime.UtcNow),
["D"]=new("d",2,true,DateTime.UtcNow), ["B"]=new("b",3,true,DateTime.UtcNow) })
{ Temps = new[] {
new TagSample("tica-6111a", 110, true, DateTime.UtcNow), // A
new TagSample("ti-6111b", 95, true, DateTime.UtcNow), // B (C보다 차가움 → B-C 역전)
new TagSample("ti-6111c", 100, true, DateTime.UtcNow), // C
new TagSample("ti-6111d", 90, true, DateTime.UtcNow) }};// D(제외)
// D(ti-6111d) 제외 후 A=110, B=95, C=100 → B-C = 95-100 = -5 < -0.5 → 온도역전(B-C)
// + vloss 불일치(Balanced=0) → corroborated=false → severe 아님
private static PvSnapshot BalancedNormalTemps() => new(
new TagSample("f", 100, true, DateTime.UtcNow), null, Array.Empty<TagSample>(),
new Dictionary<string, TagSample> {
["P"]=new("p",95,true,DateTime.UtcNow), ["R"]=new("r",760,true,DateTime.UtcNow),
["D"]=new("d",2,true,DateTime.UtcNow), ["B"]=new("b",3,true,DateTime.UtcNow) })
{ Temps = new[] {
new TagSample("tica-6111a", 110, true, DateTime.UtcNow), // 하단
new TagSample("ti-6111b", 105, true, DateTime.UtcNow), // 중간
new TagSample("ti-6111c", 100, true, DateTime.UtcNow), // 제품 pivot
new TagSample("ti-6111d", 90, true, DateTime.UtcNow) }};// 탑정
[Fact]
public void Inversion_with_vloss_triggers_recovery()
{
// WP4 역전(vloss 코러보) → severe → 전환류 진입
var cfg = Cfg(autoArm: true) with
{
TempTags = new[] { "tica-6111a", "ti-6111b", "ti-6111c", "ti-6111d" },
DTdP = 0.0, PRef = double.NaN, PressureTag = null
};
var engine = new FeedforwardEngine(); var st = new ColumnState();
// 먼저 정상 온도로 시드(spanRef)
var s0 = engine.Tick(cfg, BalancedNormalTemps(), st, DateTime.UtcNow);
// 역전 + vloss 상태 지속 → timer 누적 → Recovering
string? entryReason = null;
string? firstTp = null;
AdvisoryResult res = null!;
for (int i = 0; i < 6; i++)
{
res = engine.Tick(cfg, ImbalancedWithInversion(), st, DateTime.UtcNow);
if (i == 0) firstTp = res.TempProfileState;
if (res.Mode == ColumnMode.Recovering && entryReason is null)
entryReason = res.ModeReason;
}
Assert.Equal("온도역전", firstTp);
Assert.Equal(ColumnMode.Recovering, res.Mode);
Assert.Contains("온도역전", entryReason ?? "");
}
[Fact]
public void Inversion_alone_not_severe_sensor_check()
{
// WP4 역전만(balanced, vloss=0, ΔP 없음) → severe 아님, 센서 점검 메시지
var cfg = Cfg(autoArm: true) with
{
TempTags = new[] { "tica-6111a", "ti-6111b", "ti-6111c", "ti-6111d" },
DTdP = 0.0, PRef = double.NaN, PressureTag = null
};
var engine = new FeedforwardEngine(); var st = new ColumnState();
// 정상 온도로 시드
engine.Tick(cfg, BalancedNormalTemps(), st, DateTime.UtcNow);
// 역전 + balanced(no vloss) → timer 누적 안 됨 → Normal + 센서 점검
for (int i = 0; i < 6; i++)
{
var res = engine.Tick(cfg, BalancedWithInversion(), st, DateTime.UtcNow);
if (i == 5)
{
Assert.Equal(ColumnMode.Normal, res.Mode);
Assert.Contains("센서 점검", res.ModeReason ?? "");
}
}
}
}