feat(report): 연계운전 통합 폐합 메트릭 mass_balance_closure_linked
이송 배관으로 묶인 두 컬럼(C-9111↔C-10111)을 하나의 envelope로 보는 통합 물질수지. 한쪽 feed≈0(인출/이송)인 연계운전일엔 단독 폐합이 수학적으로 N/A지만, 양 컬럼의 외부 feed 합 / 최종 출력 합으로 보면 닫힌다. 내부 이송 스트림은 어느 컬럼 계량에도 안 잡혀(비계량) envelope에서 자동 상쇄. - Report:Transfer로 파트너 해석(없으면 미정의 → 단독컬럼엔 error). C-9111↔C-10111만 설정. - cleaning 마스크 미적용(연계 시 feed≈0이 정상 → drawdown 마스크 쓰면 전구간 제외). - summary는 TransferPartner 있는 컬럼에만 추가, 웹에 통합 폐합 신뢰블록, 토큰 치트시트. 검증(2026-05-15 C-9111): IN 23,975.9 / OUT 23,923.8 / 폐합 99.78% (수동검증 일치). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -128,8 +128,11 @@ GROUP BY b ORDER BY b";
|
|||||||
string source = "history_table", string period = "DAILY", int? sessionId = null, CancellationToken ct = default)
|
string source = "history_table", string period = "DAILY", int? sessionId = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var d = (date ?? DateTime.UtcNow.AddHours(9).AddDays(-1)).Date; // 기본 = 어제(KST)
|
var d = (date ?? DateTime.UtcNow.AddHours(9).AddDays(-1)).Date; // 기본 = 어제(KST)
|
||||||
|
var metricNames = new List<string>(SUMMARY_METRICS);
|
||||||
|
if (_map.TransferPartner(column) is not null) // 이송 연계 컬럼(C-9111↔C-10111)만 통합 폐합 추가
|
||||||
|
metricNames.Add("mass_balance_closure_linked");
|
||||||
var results = new List<MetricResultDto>();
|
var results = new List<MetricResultDto>();
|
||||||
foreach (var m in SUMMARY_METRICS)
|
foreach (var m in metricNames)
|
||||||
results.Add(await _metrics.ComputeAsync(new MetricRequestDto
|
results.Add(await _metrics.ComputeAsync(new MetricRequestDto
|
||||||
{
|
{
|
||||||
Column = column, Metric = m, PeriodDateKst = d, Period = period,
|
Column = column, Metric = m, PeriodDateKst = d, Period = period,
|
||||||
|
|||||||
@@ -197,6 +197,29 @@ function renderSummary(d) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 연계운전 통합 폐합 (이송으로 묶인 두 컬럼 합산) — 결과 있을 때만
|
||||||
|
const lk = by['mass_balance_closure_linked'];
|
||||||
|
if (lk) {
|
||||||
|
const pct = lk.Status === 'ok' ? lk.Value : null;
|
||||||
|
const ok = pct != null && pct >= 98 && pct <= 101;
|
||||||
|
const color = pct == null ? '#888' : (ok ? '#2ea043' : '#e5534b');
|
||||||
|
const e = lk.Extra || {};
|
||||||
|
html += `
|
||||||
|
<div style="border:1px dashed ${color};border-radius:8px;padding:12px;margin-bottom:16px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;color:var(--t2)">통합 물질수지 폐합 (이송 연계 컬럼 합산)</div>
|
||||||
|
<div><b>통합 IN</b> ${rpFmt(e.feed_qv,'kg')} → <b>통합 OUT</b> ${rpFmt(e.out_total,'kg')} kg</div>
|
||||||
|
<div style="font-size:11px;color:var(--t2);margin-top:2px">단독 폐합이 N/A(연계운전 feed≈0)인 날 두 컬럼을 합쳐 검산</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right">
|
||||||
|
<div style="font-size:26px;font-weight:700;color:${color}">${pct == null ? 'N/A' : rpFmt(pct,'pct') + '%'}</div>
|
||||||
|
<div style="font-size:11px;color:var(--t2)">${pct == null ? (lk.Error || '') : (ok ? '✓ 정상' : '⚠ 이탈')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// 메트릭 표
|
// 메트릭 표
|
||||||
const rows = [
|
const rows = [
|
||||||
['생산량 (제품 적산Δ)', by['production_total'], 'kg'],
|
['생산량 (제품 적산Δ)', by['production_total'], 'kg'],
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
{{ metric=mass_balance_closure; column=C-6111; field=feed_qv }} → 원료 적산 Δ (IN)
|
{{ metric=mass_balance_closure; column=C-6111; field=feed_qv }} → 원료 적산 Δ (IN)
|
||||||
{{ metric=mass_balance_closure; column=C-6111; field=out_total }} → 회수 합 (OUT)
|
{{ metric=mass_balance_closure; column=C-6111; field=out_total }} → 회수 합 (OUT)
|
||||||
{{ metric=mass_balance_closure; column=C-6111; field=out0_qv }} → 제품(경비/중비는 out1/out2)
|
{{ metric=mass_balance_closure; column=C-6111; field=out0_qv }} → 제품(경비/중비는 out1/out2)
|
||||||
|
{{ metric=mass_balance_closure_linked; column=C-9111 }} → 통합 폐합 % (이송 연계 C-9111+C-10111 합산)
|
||||||
|
|
||||||
■ PV 기반 (근사)
|
■ PV 기반 (근사)
|
||||||
{{ metric=control_residual; column=C-6111 }} → 평균 잔차(PV-SP)
|
{{ metric=control_residual; column=C-6111 }} → 평균 잔차(PV-SP)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ public sealed record QvSpec(string Unit, QvKind Kind, string A, string? B, IRead
|
|||||||
/// <summary>비정상운전(cleaning/drawdown) 제외 마스크. 진공高 또는 제품~0 또는 feed~0인 분 제외.</summary>
|
/// <summary>비정상운전(cleaning/drawdown) 제외 마스크. 진공高 또는 제품~0 또는 feed~0인 분 제외.</summary>
|
||||||
public sealed record CleaningSpec(string? VacTag, double VacMax, string ProductTag, double ProductMin, string FeedTag, double FeedMin);
|
public sealed record CleaningSpec(string? VacTag, double VacMax, string ProductTag, double ProductMin, string FeedTag, double FeedMin);
|
||||||
|
|
||||||
|
/// <summary>연계운전 통합 폐합. 이송으로 묶인 두 컬럼의 외부 feed 합 / 최종 출력 합. 내부 이송은 비계량이라 자동 상쇄.</summary>
|
||||||
|
public sealed record LinkedClosureSpec(string Unit, IReadOnlyList<string> Feeds, IReadOnlyList<string> Outputs, string Partner);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 컬럼→태그 매핑. 기존 appsettings `SteamAdvisor:Columns`(Feed/Product/TC/SteamOp/SteamFlow)를
|
/// 컬럼→태그 매핑. 기존 appsettings `SteamAdvisor:Columns`(Feed/Product/TC/SteamOp/SteamFlow)를
|
||||||
/// 단일 진실원으로 재사용한다(멀티컬럼 무료). 클린범위(스파이크/드롭아웃 경계)는 tag_metadata
|
/// 단일 진실원으로 재사용한다(멀티컬럼 무료). 클린범위(스파이크/드롭아웃 경계)는 tag_metadata
|
||||||
@@ -182,6 +185,32 @@ public sealed class ReportColumnMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 연계운전 통합 폐합 해석. Report:Transfer:{col}로 이송 파트너를 찾아, 양 컬럼의
|
||||||
|
/// Report:Closure(Feed + Outputs).QV를 합쳐 envelope 폐합을 만든다. 파트너 없으면 false
|
||||||
|
/// (= 배관 연계 없는 단독 컬럼 → 통합 폐합 미정의). C-9111↔C-10111 한 쌍에만 설정됨.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryResolveLinkedClosure(string column, out LinkedClosureSpec? spec)
|
||||||
|
{
|
||||||
|
spec = null;
|
||||||
|
var partner = _config[$"Report:Transfer:{column}"];
|
||||||
|
if (string.IsNullOrWhiteSpace(partner)) return false;
|
||||||
|
|
||||||
|
var feeds = new List<string>();
|
||||||
|
var outs = new List<string>();
|
||||||
|
foreach (var col in new[] { column, partner })
|
||||||
|
{
|
||||||
|
var cl = _config.GetSection($"Report:Closure:{col}");
|
||||||
|
if (!cl.Exists()) return false;
|
||||||
|
if (ToQv(cl["Feed"]) is string f) feeds.Add(f);
|
||||||
|
foreach (var o in cl.GetSection("Outputs").Get<string[]>() ?? Array.Empty<string>())
|
||||||
|
if (ToQv(o) is string oq) outs.Add(oq);
|
||||||
|
}
|
||||||
|
if (feeds.Count == 0 || outs.Count == 0) return false;
|
||||||
|
spec = new LinkedClosureSpec("%", feeds, outs, partner);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>cleaning/drawdown 마스크 해석. 제품·feed는 SteamAdvisor:Columns, 진공태그·임계는 Report:Cleaning.</summary>
|
/// <summary>cleaning/drawdown 마스크 해석. 제품·feed는 SteamAdvisor:Columns, 진공태그·임계는 Report:Cleaning.</summary>
|
||||||
public bool TryResolveCleaning(string column, out CleaningSpec? spec)
|
public bool TryResolveCleaning(string column, out CleaningSpec? spec)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ public sealed class ReportMetricService : IReportMetricService
|
|||||||
{
|
{
|
||||||
await DynamicsAsync(res, conn, tbl, req.Column, fromUtc, toUtc, isFast, req.SessionId, ct);
|
await DynamicsAsync(res, conn, tbl, req.Column, fromUtc, toUtc, isFast, req.SessionId, ct);
|
||||||
}
|
}
|
||||||
|
else if (req.Metric == "mass_balance_closure_linked")
|
||||||
|
{
|
||||||
|
if (!_map.TryResolveLinkedClosure(req.Column, out var lspec) || lspec is null)
|
||||||
|
{ res.Status = "error"; res.Error = $"연계 폐합 미정의(이송 파트너 없음): {req.Column}"; res.Value = null; return res; }
|
||||||
|
res.Unit = lspec.Unit;
|
||||||
|
await ComputeLinkedClosureAsync(res, conn, tbl, lspec, fromUtc, toUtc, isFast, req.SessionId, ct);
|
||||||
|
}
|
||||||
else if (QV_METRICS.Contains(req.Metric))
|
else if (QV_METRICS.Contains(req.Metric))
|
||||||
{
|
{
|
||||||
if (!_map.TryResolveQv(req.Column, req.Metric, out var qspec) || qspec is null)
|
if (!_map.TryResolveQv(req.Column, req.Metric, out var qspec) || qspec is null)
|
||||||
@@ -207,6 +214,33 @@ FROM seq2";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 연계운전 통합 폐합: 두 컬럼 외부 feed 합 / 최종 출력 합. cleaning 마스크 미적용(null) —
|
||||||
|
// 연계 시 한쪽 feed≈0은 정상(drawdown)이라 마스크를 쓰면 전구간 제외됨. 내부 이송은
|
||||||
|
// 비계량(어느 컬럼 feed/output에도 안 잡힘)이라 envelope에서 자동 상쇄. ──
|
||||||
|
private async Task ComputeLinkedClosureAsync(MetricResultDto res, DbConnection conn, string tbl,
|
||||||
|
LinkedClosureSpec s, DateTime fromUtc, DateTime toUtc, bool isFast, int? sid, CancellationToken ct)
|
||||||
|
{
|
||||||
|
double inSum = 0; int nMin = 0;
|
||||||
|
foreach (var f in s.Feeds)
|
||||||
|
{
|
||||||
|
var (d, n, _) = await QvDeltaAsync(conn, tbl, f, null, fromUtc, toUtc, isFast, sid, ct);
|
||||||
|
inSum += d ?? 0; nMin = Math.Max(nMin, n);
|
||||||
|
}
|
||||||
|
if (inSum <= 0) { res.Status = "no_data"; res.Value = null; res.Error = "통합 IN(외부 feed 합) Δ≤0"; return; }
|
||||||
|
|
||||||
|
double outSum = 0;
|
||||||
|
for (int i = 0; i < s.Outputs.Count; i++)
|
||||||
|
{
|
||||||
|
var (d, _, _) = await QvDeltaAsync(conn, tbl, s.Outputs[i], null, fromUtc, toUtc, isFast, sid, ct);
|
||||||
|
outSum += d ?? 0;
|
||||||
|
res.Extra[$"out{i}_qv"] = d;
|
||||||
|
}
|
||||||
|
res.Value = 100.0 * outSum / inSum; // 통합 폐합 %
|
||||||
|
res.N = nMin;
|
||||||
|
res.Extra["feed_qv"] = inSum; // 통합 IN
|
||||||
|
res.Extra["out_total"] = outSum; // 통합 OUT
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 적산 Δ. 비정상운전 분(cleaning=진공高/제품~0, drawdown=feed~0) 제외 후, 정상구간 양(+)증분만 합산(cap 5e4).
|
/// 적산 Δ. 비정상운전 분(cleaning=진공高/제품~0, drawdown=feed~0) 제외 후, 정상구간 양(+)증분만 합산(cap 5e4).
|
||||||
/// 리셋 3종 자동처리: 999999 wrap·cleaning 리셋·운전조건변경 리셋. cl=null이면 마스크 없이 양증분합산.
|
/// 리셋 3종 자동처리: 999999 wrap·cleaning 리셋·운전조건변경 리셋. cl=null이면 마스크 없이 양증분합산.
|
||||||
|
|||||||
Reference in New Issue
Block a user