Phase II:
- FfOperatorAction entity + ff_operator_action DDL/DbSet
- IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN)
- IFeedforwardAuditService + FeedforwardAuditService (raw ADO insert/query)
- FeedforwardSupervisor.AutoWriteAsync (per-stream OPC UA after Tick, rate-limited)
- FeedforwardConfigStore: advisory_only now read/writes DB, sp_node_id column
- FeedforwardController: auth (X-Kb-Token) on config/delete/write/audit;
POST write/{id}/{key} manual SP write; GET audit; write results in MapColumn
- ff.js: token header, auto-write badge, per-stream write result, spNodeId, advisoryOnly
- ff.css: .ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
- Program.cs: register audit (Scoped) + write guard (Singleton)
WO-2~7 (build 0W/0E, test 22/22):
- PCT monitor, θ auto-tune, slow bias, front position indicator,
total reflux recovery, config form expansion
74 lines
3.1 KiB
C#
74 lines
3.1 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);
|
|
}
|
|
}
|