167 lines
7.6 KiB
C#
167 lines
7.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using ExperionCrawler.Core.Application.Feedforward;
|
|
using ExperionCrawler.Infrastructure.Control;
|
|
using Xunit;
|
|
|
|
namespace ExperionCrawler.Tests;
|
|
|
|
public class FeedforwardTempTests
|
|
{
|
|
// ── 순수 블록 ────────────────────────────────────────────────
|
|
[Fact]
|
|
public void TempCorrection_compensates_pressure()
|
|
{
|
|
Assert.Equal(99.0, TempCorrection.PressureCompensated(100, p: 52, pRef: 50, dTdP: 0.5), 6);
|
|
Assert.Equal(100.0, TempCorrection.PressureCompensated(100, p: 52, pRef: 50, dTdP: 0.0), 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiffTemp_delta_and_double()
|
|
{
|
|
Assert.Equal(2.0, DiffTemp.Delta(81, 79), 6);
|
|
Assert.Equal(0.0, DiffTemp.Double(82, 81, 80), 6);
|
|
Assert.Equal(1.0, DiffTemp.Double(83, 81, 80), 6);
|
|
}
|
|
|
|
// ── 엔진 배선 ────────────────────────────────────────────────
|
|
private static ColumnConfig Cfg(double dtdp, double pRef) => new()
|
|
{
|
|
Id = 1, Name = "C-TEMP", Enabled = true, FeedTag = "f", ProductKey = "P",
|
|
ScanSec = 2, DTdP = dtdp, PRef = pRef, PressureTag = "p",
|
|
TempTags = new[] { "t1" },
|
|
Streams = new[] { new StreamConfig { Key = "P", FlowTag = "ficq-6118", Role = StreamRole.Commanded, Grade = Confidence.A, TargetCoeff = 0.95 } }
|
|
};
|
|
|
|
private static PvSnapshot Snap(double pressure, double temp) => new(
|
|
new TagSample("f", 100, true, DateTime.UtcNow),
|
|
new TagSample("p", pressure, true, DateTime.UtcNow),
|
|
Array.Empty<TagSample>(),
|
|
new Dictionary<string, TagSample> { ["P"] = new TagSample("ficq-6118", 95, true, DateTime.UtcNow)})
|
|
{ Temps = new[] { new TagSample("t1", temp, true, DateTime.UtcNow) } };
|
|
|
|
[Fact]
|
|
public void Engine_populates_pct_with_explicit_pref()
|
|
{
|
|
var res = new FeedforwardEngine().Tick(Cfg(dtdp: 0.5, pRef: 50), Snap(pressure: 52, temp: 100),
|
|
new ColumnState(), DateTime.UtcNow);
|
|
Assert.NotNull(res.Temps);
|
|
var tp = res.Temps![0];
|
|
Assert.Equal("t1", tp.Tag);
|
|
Assert.Equal(100.0, tp.Raw, 6);
|
|
Assert.Equal(99.0, tp.Pct, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Engine_seeds_pref_on_first_tick_when_nan()
|
|
{
|
|
var engine = new FeedforwardEngine();
|
|
var st = new ColumnState();
|
|
var r1 = engine.Tick(Cfg(dtdp: 0.5, pRef: double.NaN), Snap(pressure: 50, temp: 100), st, DateTime.UtcNow);
|
|
Assert.Equal(100.0, r1.Temps![0].Pct, 6);
|
|
var r2 = engine.Tick(Cfg(dtdp: 0.5, pRef: double.NaN), Snap(pressure: 54, temp: 100), st, DateTime.UtcNow);
|
|
Assert.Equal(98.0, r2.Temps![0].Pct, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Engine_no_pct_when_dtdp_zero()
|
|
{
|
|
var res = new FeedforwardEngine().Tick(Cfg(dtdp: 0.0, pRef: 50), Snap(pressure: 80, temp: 100),
|
|
new ColumnState(), DateTime.UtcNow);
|
|
Assert.Equal(100.0, res.Temps![0].Pct, 6);
|
|
}
|
|
|
|
// ── WP6: 압력 프로파일 PCT + ΔP 합성 ──────────────────────────
|
|
private static ColumnConfig CfgProfile(double dtdp, double pRef) => new()
|
|
{
|
|
Id = 2, Name = "C-PROFILE", Enabled = true, FeedTag = "f", ProductKey = "P",
|
|
ScanSec = 2, DTdP = dtdp, PRef = pRef, PressureTag = "pica",
|
|
TempTags = new[] { "ta", "tb", "tc", "td" },
|
|
Streams = new[] { new StreamConfig { Key = "P", FlowTag = "ficq-6118", Role = StreamRole.Commanded, Grade = Confidence.A, TargetCoeff = 0.95 } }
|
|
};
|
|
|
|
// 4 temps bottom→top, with explicit bottom pressure
|
|
private static PvSnapshot SnapProfile(double pTop, double pBottom, double ta, double tb, double tc, double td) => new(
|
|
new TagSample("f", 100, true, DateTime.UtcNow),
|
|
new TagSample("pica", pTop, true, DateTime.UtcNow),
|
|
Array.Empty<TagSample>(),
|
|
new Dictionary<string, TagSample> { ["P"] = new TagSample("ficq-6118", 95, true, DateTime.UtcNow) })
|
|
{
|
|
Temps = new[] {
|
|
new TagSample("ta", ta, true, DateTime.UtcNow),
|
|
new TagSample("tb", tb, true, DateTime.UtcNow),
|
|
new TagSample("tc", tc, true, DateTime.UtcNow),
|
|
new TagSample("td", td, true, DateTime.UtcNow)
|
|
},
|
|
BottomPressure = new TagSample("pi-b", pBottom, true, DateTime.UtcNow)
|
|
};
|
|
|
|
[Fact]
|
|
public void Local_pressure_gives_different_pct_per_temp()
|
|
{
|
|
// dTdP≠0 + 프로파일 압력 → 각 temp가 다른 국소압으로 보정
|
|
// pTop=50, pBottom=60 → 국소압: ta(하단)=60, tb=56.7, tc=53.3, td(상단)=50
|
|
// pRef=50 → pRefLocal=50(단일), 보정: pct=raw - dTdP*(pLocal-pRef)
|
|
// ta: 100 - 0.5*(60-50) = 100 - 5 = 95
|
|
// tb: 100 - 0.5*(56.7-50) = 100 - 3.3 = 96.7
|
|
// tc: 100 - 0.5*(53.3-50) = 100 - 1.7 = 98.3
|
|
// td: 100 - 0.5*(50-50) = 100 - 0 = 100
|
|
var engine = new FeedforwardEngine();
|
|
var st = new ColumnState();
|
|
var res = engine.Tick(CfgProfile(dtdp: 0.5, pRef: 50),
|
|
SnapProfile(pTop: 50, pBottom: 60, ta: 100, tb: 100, tc: 100, td: 100), st, DateTime.UtcNow);
|
|
Assert.NotNull(res.Temps);
|
|
Assert.Equal(4, res.Temps!.Count);
|
|
Assert.Equal(95.0, res.Temps[0].Pct, 1); // ta(하단), bottom=60
|
|
Assert.Equal(96.7, res.Temps[1].Pct, 1); // tb
|
|
Assert.Equal(98.3, res.Temps[2].Pct, 1); // tc
|
|
Assert.Equal(100.0, res.Temps[3].Pct, 1); // td(상단), top=50
|
|
}
|
|
|
|
[Fact]
|
|
public void Synthetic_deltaP_feeds_applyRecovery_sigDp()
|
|
{
|
|
// ΔP > DeltaPFloodLimit (+ vloss) → sigDp로 recovery 진입
|
|
var cfg = CfgProfile(dtdp: 0.0, pRef: double.NaN) with
|
|
{
|
|
RecoveryEnabled = true, RecoveryAutoArm = true,
|
|
ImbalanceTriggerFrac = 0.10, ImbalanceTriggerSec = 4,
|
|
DeltaPFloodLimit = 5.0 // ΔP > 5 → flooding
|
|
};
|
|
// vloss: P=30 out of feed=100 → sigVloss 확보
|
|
var snap = new PvSnapshot(
|
|
new TagSample("f", 100, true, DateTime.UtcNow),
|
|
new TagSample("pica", 50, true, DateTime.UtcNow),
|
|
Array.Empty<TagSample>(),
|
|
new Dictionary<string, TagSample> {
|
|
["P"] = new("ficq-6118", 30, true, DateTime.UtcNow) })
|
|
{
|
|
Temps = new[] { new TagSample("ta", 100, true, DateTime.UtcNow) },
|
|
BottomPressure = new TagSample("pi-b", 60, true, DateTime.UtcNow),
|
|
DeltaP = new TagSample("_syn", 10, true, DateTime.UtcNow) // ΔP = 60-50 = 10 > 5
|
|
};
|
|
var engine = new FeedforwardEngine();
|
|
var st = new ColumnState();
|
|
for (int i = 0; i < 6; i++)
|
|
engine.Tick(cfg, snap, st, DateTime.UtcNow);
|
|
Assert.Equal(ColumnMode.Recovering, st.Mode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Local_pressure_off_when_bottom_missing()
|
|
{
|
|
// BottomPressure 없음 → 국소압 없이 단일 pTop 사용 → dTdP 적용 시 모두 동일 PCT
|
|
var noBottom = SnapProfile(pTop: 50, pBottom: 60, ta: 100, tb: 100, tc: 100, td: 100) with
|
|
{
|
|
BottomPressure = null
|
|
};
|
|
var engine = new FeedforwardEngine();
|
|
var st = new ColumnState();
|
|
var res = engine.Tick(CfgProfile(dtdp: 0.5, pRef: 50), noBottom, st, DateTime.UtcNow);
|
|
Assert.NotNull(res.Temps);
|
|
// 모두 단일 pTop=50으로 보정 → pct=100 (pLocal=pRef=50)
|
|
foreach (var tp in res.Temps!)
|
|
Assert.Equal(100.0, tp.Pct, 6);
|
|
}
|
|
}
|