Files
ExperionCrawler/src/Infrastructure/Control/FeedforwardEngine.cs

164 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ExperionCrawler.Core.Application.Feedforward;
namespace ExperionCrawler.Infrastructure.Control;
public sealed class StreamState
{
public DeadTimeBuffer Dead { get; } = new();
public FirstOrderLag Lag { get; } = new();
public RateLimiter Rate { get; } = new();
public double LastRec { get; set; } = double.NaN;
}
public sealed class ColumnState
{
public FirstOrderLag FeedFilter { get; } = new();
public FirstOrderLag PressFilter { get; } = new();
public Derivative FeedDeriv { get; } = new();
public double SettleTimerSec { get; set; }
public bool Initialized { get; set; }
public Dictionary<string, StreamState> Streams { get; } = new();
public StreamState Stream(string key)
{
if (!Streams.TryGetValue(key, out var s)) { s = new StreamState(); Streams[key] = s; }
return s;
}
}
public sealed class FeedforwardEngine
{
public AdvisoryResult Tick(ColumnConfig cfg, PvSnapshot pv, ColumnState st, DateTime now)
{
var ts = cfg.ScanSec;
if (!pv.Feed.Good || !Num.IsFinite(pv.Feed.Value))
return Hold(cfg, st, now, "FEED BAD");
var ff = st.FeedFilter.Step(pv.Feed.Value, cfg.FeedFilterTauSec, ts);
if (!st.Initialized) { SeedAll(cfg, pv, st, ff); st.Initialized = true; }
var dF = st.FeedDeriv.Update(ff, ts);
bool moving = cfg.FeedMoveThresholdPerMin > 0
&& Math.Abs(dF) * 60.0 > cfg.FeedMoveThresholdPerMin;
bool pUnstable = false;
if (pv.Pressure is { Good: true } pp && Num.IsFinite(pp.Value))
{
var pf = st.PressFilter.Step(pp.Value, cfg.PressFilterTauSec, ts);
pUnstable = Math.Abs(pp.Value - pf) > cfg.PressureBand;
}
if (moving || pUnstable) st.SettleTimerSec = cfg.SettleSec;
else st.SettleTimerSec = Math.Max(0.0, st.SettleTimerSec - ts);
bool transient = moving || pUnstable || st.SettleTimerSec > 0.0;
string treason = moving ? "FEED 이동"
: pUnstable ? "압력 불안정"
: st.SettleTimerSec > 0.0 ? $"정착 대기 {st.SettleTimerSec:F0}s" : "";
var outs = new List<StreamAdvisory>(cfg.Streams.Count);
double? prodRec = null;
foreach (var s in cfg.Streams)
{
if (s.RefluxFromProduct) continue;
var (rec, note) = ComputeStream(s, ff, dF, ts, st.Stream(s.Key));
if (s.Key == cfg.ProductKey) prodRec = rec;
outs.Add(BuildAdvisory(s, pv, rec, note, transient, st.Stream(s.Key)));
}
foreach (var s in cfg.Streams)
{
if (!s.RefluxFromProduct) continue;
var stt = st.Stream(s.Key);
double? rec = null;
if (prodRec is double p)
{
var raw = Num.Clamp(s.TargetCoeff * p, s.SpMin, s.SpMax);
rec = stt.Rate.Step(raw, s.RateUpPerMin, s.RateDnPerMin, ts);
}
outs.Add(BuildAdvisory(s, pv, rec, "외부환류 R=R_f×P (P 지연 상속)", transient, stt));
}
double? vloss = null, yield = null;
string mbState;
if (transient)
mbState = $"정착 대기 ({st.SettleTimerSec:F0}s)";
else if (TryStreamPv(pv, "D", out var d) && TryStreamPv(pv, "P", out var pp2)
&& TryStreamPv(pv, "B", out var b) && ff > 1e-6)
{
vloss = ff - (d + pp2 + b);
yield = 100.0 * pp2 / ff;
mbState = Math.Abs(vloss.Value) > 0.03 * ff ? "물질수지 불일치(계측 점검)"
: vloss.Value < 0 ? "음의 손실(스팬 오류 의심)"
: "정상";
}
else mbState = "입력 부족";
return new AdvisoryResult(cfg.Id, cfg.Name, now, cfg.Enabled,
transient, treason, ff, outs, vloss, yield, mbState);
}
private static (double? rec, string note) ComputeStream(
StreamConfig s, double ff, double dF, double ts, StreamState stt)
{
switch (s.Role)
{
case StreamRole.Commanded:
double theta = dF >= 0 ? s.ThetaUpSec : s.ThetaDnSec;
double fd = stt.Dead.Through(ff, theta, ts);
fd = stt.Lag.Step(fd, s.TauSec, ts);
double raw = Num.Clamp(s.TargetCoeff * fd, s.SpMin, s.SpMax);
double rec = stt.Rate.Step(raw, s.RateUpPerMin, s.RateDnPerMin, ts);
return (rec, "");
case StreamRole.LevelDriven:
return (s.TargetCoeff * ff, "레벨 제어 구동 — 기대치(deadtime 미적용)");
default:
return (null, "모니터(SP 없음)");
}
}
private static StreamAdvisory BuildAdvisory(
StreamConfig s, PvSnapshot pv, double? rec, string note, bool transient, StreamState stt)
{
double curPv = pv.Streams.TryGetValue(s.Key, out var smp) && smp.Good ? smp.Value : double.NaN;
int trend = rec is double r && Num.IsFinite(stt.LastRec)
? Math.Sign(r - stt.LastRec) : 0;
if (rec is double rr) stt.LastRec = rr;
double? gap = (rec is double g && Num.IsFinite(curPv)) ? g - curPv : null;
return new StreamAdvisory(s.Key, s.FlowTag, s.Role, curPv, rec, gap, trend,
!transient && s.Role != StreamRole.Monitor, s.Grade, s.LevelTag, note);
}
private static bool TryStreamPv(PvSnapshot pv, string key, out double v)
{
v = double.NaN;
if (pv.Streams.TryGetValue(key, out var s) && s.Good && Num.IsFinite(s.Value)) { v = s.Value; return true; }
return false;
}
private static void SeedAll(ColumnConfig cfg, PvSnapshot pv, ColumnState st, double ff)
{
st.FeedDeriv.Update(ff, cfg.ScanSec);
foreach (var s in cfg.Streams)
{
var stt = st.Stream(s.Key);
double seed = pv.Streams.TryGetValue(s.Key, out var smp) && smp.Good ? smp.Value : ff * s.TargetCoeff;
stt.Lag.Seed(seed);
stt.Rate.Seed(seed);
stt.LastRec = seed;
}
}
private AdvisoryResult Hold(ColumnConfig cfg, ColumnState st, DateTime now, string reason)
{
var outs = cfg.Streams.Select(s =>
{
var stt = st.Stream(s.Key);
double? rec = Num.IsFinite(stt.LastRec) ? stt.LastRec : (double?)null;
return new StreamAdvisory(s.Key, s.FlowTag, s.Role, double.NaN, rec, null, 0,
false, s.Grade, s.LevelTag, $"홀드: {reason}");
}).ToList();
return new AdvisoryResult(cfg.Id, cfg.Name, now, cfg.Enabled, true, reason,
st.FeedFilter.Value, outs, null, null, $"홀드: {reason}");
}
}