feat: Phase II auto-write (WriteGuard, audit, auth) + WO-2~7 완료
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
This commit is contained in:
48
tests/ExperionCrawler.Tests/FeedforwardBiasTests.cs
Normal file
48
tests/ExperionCrawler.Tests/FeedforwardBiasTests.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ExperionCrawler.Core.Application.Feedforward;
|
||||
using ExperionCrawler.Infrastructure.Control;
|
||||
using Xunit;
|
||||
|
||||
namespace ExperionCrawler.Tests;
|
||||
|
||||
public class FeedforwardBiasTests
|
||||
{
|
||||
private static ColumnConfig Cfg() => new()
|
||||
{
|
||||
Id = 1, Name = "C-BIAS", Enabled = true, FeedTag = "f", ProductKey = "P",
|
||||
ScanSec = 2, BiasMaWindowSec = 20, // 10 샘플 창
|
||||
Streams = new[]
|
||||
{
|
||||
new StreamConfig { Key = "P", FlowTag = "p", Role = StreamRole.Commanded, Grade = Confidence.A, TargetCoeff = 0.95 },
|
||||
new StreamConfig { Key = "D", FlowTag = "d", Role = StreamRole.LevelDriven, Grade = Confidence.B, TargetCoeff = 0.02 },
|
||||
new StreamConfig { Key = "B", FlowTag = "b", Role = StreamRole.LevelDriven, Grade = Confidence.B, TargetCoeff = 0.03 },
|
||||
}
|
||||
};
|
||||
|
||||
// FEED 100 고정, P=95 → K_obs ≈ 0.95, D/B는 물질수지 충족용
|
||||
private static PvSnapshot Snap() => new(
|
||||
new TagSample("f", 100, true, DateTime.UtcNow), null, Array.Empty<TagSample>(),
|
||||
new Dictionary<string, TagSample> {
|
||||
["P"] = new("p", 95, true, DateTime.UtcNow),
|
||||
["D"] = new("d", 2, true, DateTime.UtcNow),
|
||||
["B"] = new("b", 3, true, DateTime.UtcNow),
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void KObs_and_VLossMa_accumulate_in_steady_state()
|
||||
{
|
||||
var engine = new FeedforwardEngine();
|
||||
var st = new ColumnState();
|
||||
AdvisoryResult res = engine.Tick(Cfg(), Snap(), st, DateTime.UtcNow);
|
||||
for (int i = 0; i < 20; i++) res = engine.Tick(Cfg(), Snap(), st, DateTime.UtcNow);
|
||||
|
||||
var p = res.Streams.First(s => s.Key == "P");
|
||||
Assert.NotNull(p.KObsSuggest);
|
||||
Assert.InRange(p.KObsSuggest!.Value, 0.94, 0.96); // 95/100
|
||||
|
||||
Assert.NotNull(res.VLossMa);
|
||||
Assert.InRange(res.VLossMa!.Value, -0.5, 0.5); // 100-(95+2+3)=0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user