WP3 — read-only Feed Ramp Advisor (쓰기 없음): - FeedRampCalculator(순수함수): ceiling(밸브포화/flooding)·램프율(valveSlew/dynamic)·예상시간·binding·스팀(FIQ-6115 목표, a안 현열보정) 산출 - FeedRampModels(DTO+ISimOverrideStore), FeedRampAdvisorService(라이브+override), GET /api/ff/ramp-advisor WP0 — Sim Override Layer: - SimOverrideStore(ConcurrentDictionary+volatile), sim/override GET·POST·DELETE, Feedforward:SimOverrideEnabled 게이트 - 한계: ramp-advisor만 통합·엔진 미반영 → S6/S7 라이브는 override 불가(문서화) WP1 — docs/안전피드램프-검증시나리오매트릭스.md (S0~S7) 검증: 단위 31/31, 라이브 스모크 S1~S5 기대치 일치(3.29 dynamic/60.8min/366.7, 31.58 valveSlew, 2105 clamp, 1018.8 flooding, 309 현열) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
3.2 KiB
C#
70 lines
3.2 KiB
C#
using System.Globalization;
|
|
using ExperionCrawler.Core.Application.Feedforward;
|
|
using ExperionCrawler.Core.Application.Interfaces;
|
|
|
|
namespace ExperionCrawler.Infrastructure.Control;
|
|
|
|
/// <summary>
|
|
/// WP3-C: 라이브 데이터 수집 + FeedRampCalculator 호출. 읽기 전용(쓰기 없음).
|
|
/// Sim Override(WP0) 활성 시 해당 태그는 override값 우선. extras 태그는 C-6111 스코프 하드코딩(작업지시서 §5).
|
|
/// </summary>
|
|
public sealed class FeedRampAdvisorService
|
|
{
|
|
private readonly IFeedforwardConfigStore _cfgStore;
|
|
private readonly IExperionDbService _db;
|
|
private readonly ISimOverrideStore _sim;
|
|
|
|
public FeedRampAdvisorService(IFeedforwardConfigStore cfgStore, IExperionDbService db, ISimOverrideStore sim)
|
|
{ _cfgStore = cfgStore; _db = db; _sim = sim; }
|
|
|
|
public async Task<FeedRampAdvisory?> ComputeAsync(
|
|
int columnId, double targetFeed, double deltaIAllow,
|
|
double sensibleGain, double feedTempRef, double floodLimit, double n, CancellationToken ct = default)
|
|
{
|
|
var cfg = (await _cfgStore.LoadAllAsync(ct)).FirstOrDefault(c => c.Id == columnId);
|
|
if (cfg is null) return null;
|
|
|
|
static string PvTag(string baseTag)
|
|
{ var t = baseTag.ToLowerInvariant(); return t.EndsWith(".pv") ? t : t + ".pv"; }
|
|
|
|
string picaTag = PvTag(cfg.PressureTag ?? "pica-6111");
|
|
const string piTag = "pi-6111b.pv", tiTag = "ti-6103.pv", steamTag = "ficq-6115.pv", opTag = "tica-6111a.op";
|
|
|
|
var tags = new List<string> { PvTag(cfg.FeedTag), picaTag, piTag, tiTag, steamTag, opTag };
|
|
tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
|
|
|
|
var rows = (await _db.GetRealtimeRecordsByTagNamesAsync(tags))
|
|
.ToDictionary(r => r.TagName.ToLowerInvariant(), r => r);
|
|
|
|
bool TryRead(string tag, out double v)
|
|
{
|
|
tag = tag.ToLowerInvariant();
|
|
if (_sim.Enabled && _sim.TryGet(tag, out v)) return true; // override(신선)
|
|
if (rows.TryGetValue(tag, out var r) && r.LiveValue is not null
|
|
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out v)
|
|
&& (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec)
|
|
return true;
|
|
v = double.NaN; return false;
|
|
}
|
|
|
|
TagSample Sample(string baseTag)
|
|
{
|
|
var tag = PvTag(baseTag);
|
|
bool ok = TryRead(tag, out var v);
|
|
return new TagSample(tag, ok ? v : double.NaN, ok, DateTime.UtcNow);
|
|
}
|
|
double? Opt(string tag) => TryRead(tag, out var v) ? v : (double?)null;
|
|
|
|
var feed = Sample(cfg.FeedTag);
|
|
var streams = cfg.Streams.ToDictionary(s => s.Key, s => Sample(s.FlowTag));
|
|
var pv = new PvSnapshot(feed, null, Array.Empty<TagSample>(), streams);
|
|
|
|
var extra = new RampExtraInputs(
|
|
Pica: Opt(picaTag), Pi6111b: Opt(piTag), Ti6103: Opt(tiTag),
|
|
Fiq6115: Opt(steamTag), Op: Opt(opTag),
|
|
FloodLimit: floodLimit, FloodExponentN: n);
|
|
|
|
return FeedRampCalculator.Compute(cfg, pv, targetFeed, deltaIAllow, sensibleGain, feedTempRef, extra, _sim.Enabled);
|
|
}
|
|
}
|