fix(historian): #680 lane A — time-weighted aggregates follow Part 13
TimeAverage/Total: Uncertain when a Bad value is skipped, an Uncertain value is used, the value is extrapolated or the interval is not covered; an interval wholly inside an outage bridges (Uncertain); Total integrates only the covered part. TimeAverage2/Total2 stop at the end of the data (Partial, then BadNoData). PercentGood/PercentBad become duration-based and move to the time-weighted family (now subject to MaxTimeWeightedRawRows). Reverse reads set Partial on the interval holding the latest sample when nothing follows (SDK m_startOfData). Parity tests gain end-of-data and outage datasets; pre-existing #680 masks removed.
This commit is contained in:
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
/// <summary>The query shape that serves an aggregate. One query per family.</summary>
|
||||
public enum AggregateFamily
|
||||
{
|
||||
/// <summary>Average / Minimum / Maximum / Count / Range / PercentGood / PercentBad: one GROUP BY.</summary>
|
||||
/// <summary>Average / Minimum / Maximum / Count / Range: one GROUP BY.</summary>
|
||||
Simple,
|
||||
|
||||
/// <summary>Start / End / Delta: first + last usable sample per bucket.</summary>
|
||||
@@ -15,7 +15,8 @@ public enum AggregateFamily
|
||||
ActualTime,
|
||||
|
||||
/// <summary>
|
||||
/// TimeAverage / TimeAverage2 / Total / Total2 / Interpolative: Part 13 time-weighted, with bounding values.
|
||||
/// TimeAverage / TimeAverage2 / Total / Total2 / Interpolative, and the duration-based PercentGood / PercentBad
|
||||
/// (#680): Part 13 time-weighted, with bounding values.
|
||||
/// The backend streams the window's samples plus the bounding rows and <see cref="TimeWeightedAggregator"/>
|
||||
/// evaluates them (identically for every backend).
|
||||
/// </summary>
|
||||
@@ -31,12 +32,12 @@ public static class AggregateFamilies
|
||||
public static AggregateFamily? FamilyOf(HistoryAggregateType aggregate) => aggregate switch
|
||||
{
|
||||
HistoryAggregateType.Average or HistoryAggregateType.Minimum or HistoryAggregateType.Maximum
|
||||
or HistoryAggregateType.Count or HistoryAggregateType.Range
|
||||
or HistoryAggregateType.PercentGood or HistoryAggregateType.PercentBad => AggregateFamily.Simple,
|
||||
or HistoryAggregateType.Count or HistoryAggregateType.Range => AggregateFamily.Simple,
|
||||
HistoryAggregateType.Start or HistoryAggregateType.End or HistoryAggregateType.Delta => AggregateFamily.StartEnd,
|
||||
HistoryAggregateType.MinimumActualTime or HistoryAggregateType.MaximumActualTime => AggregateFamily.ActualTime,
|
||||
HistoryAggregateType.TimeAverage or HistoryAggregateType.TimeAverage2 or HistoryAggregateType.Total
|
||||
or HistoryAggregateType.Total2 or HistoryAggregateType.Interpolative => AggregateFamily.TimeWeighted,
|
||||
or HistoryAggregateType.Total2 or HistoryAggregateType.Interpolative
|
||||
or HistoryAggregateType.PercentGood or HistoryAggregateType.PercentBad => AggregateFamily.TimeWeighted,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
+199
-42
@@ -4,8 +4,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
|
||||
/// <summary>
|
||||
/// The OPC UA Part 13 time-weighted aggregates (<see cref="AggregateFamily.TimeWeighted"/>): <c>TimeAverage</c>,
|
||||
/// <c>TimeAverage2</c>, <c>Total</c>, <c>Total2</c> and <c>Interpolative</c>, evaluated over a time-ordered stream of
|
||||
/// stored samples. Both SQL backends fetch the same rows and feed them here, so the two produce identical results
|
||||
/// <c>TimeAverage2</c>, <c>Total</c>, <c>Total2</c>, <c>Interpolative</c> and the duration-based <c>PercentGood</c> /
|
||||
/// <c>PercentBad</c> (#680), evaluated over a time-ordered stream of stored samples. Both SQL backends fetch the same rows and feed them here, so the two produce identical results
|
||||
/// by construction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -24,10 +24,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
/// <item><c>s</c> usable, both Double/Int64 ⇒ <b>sloped</b> (linear <c>p → s</c>); quality = worse of the two.</item>
|
||||
/// <item><c>s</c> usable, either Boolean ⇒ <b>stepped</b> (<c>p</c> held); quality = <c>p</c>'s.</item>
|
||||
/// <item><c>s</c> Bad ⇒ <c>p</c> held up to the outage (never interpolated into it, as AtTime does); Uncertain.</item>
|
||||
/// <item>no later sample at all ⇒ <c>p</c> held to the window end (stepped extrapolation); Uncertain.</item>
|
||||
/// <item>no later sample at all (nothing at or after the window end) ⇒ the data has ENDED at <c>p</c>: no
|
||||
/// extrapolation (Part 13 "After End of Data", #680).</item>
|
||||
/// </list>
|
||||
/// From a Bad sample (incl. an <c>Empty</c> outage marker) to the next sample, and before the first sample, the
|
||||
/// signal is Bad (no data). This is the <i>simple bounding</i> signal.
|
||||
/// From a Bad sample (incl. an <c>Empty</c> outage marker) to the next sample the signal is Bad (outage) — to the
|
||||
/// window end when nothing follows it; before the first sample there is no data. This is the <i>simple bounding</i>
|
||||
/// signal.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <i>interpolated bounding</i> signal (Part 13 "interpolated bounding values") skips Bad samples: it joins
|
||||
@@ -36,32 +38,58 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item><term>TimeAverage</term><description>∫ interpolated-bounding signal ÷ its defined duration in the interval.</description></item>
|
||||
/// <item><term>Total</term><description><c>TimeAverage × interval seconds</c> (value·s) — Part 13 §5.4.3.8.</description></item>
|
||||
/// <item><term>Total</term><description>∫ interpolated-bounding signal over its defined part, in value·s — Part 13
|
||||
/// §5.4.3.8 (= <c>TimeAverage × interval seconds</c> on a covered interval; only the covered part when the data
|
||||
/// starts inside it, #680).</description></item>
|
||||
/// <item><term>TimeAverage2</term><description>∫ simple-bounding signal over its non-Bad parts ÷ non-Bad duration (Bad intervals excluded).</description></item>
|
||||
/// <item><term>Total2</term><description>∫ simple-bounding signal over its non-Bad parts, in value·s (= TimeAverage2 × non-Bad seconds).</description></item>
|
||||
/// <item><term>PercentGood / PercentBad</term><description><c>100 ×</c> Good / Bad region time ÷ interval duration
|
||||
/// (Part 13 §5.4.3.31-34): stepped regions, each with the status of the sample that starts it; the region before an
|
||||
/// interval's first own sample has the simple bounding value's status (Bad after a Bad sample; Uncertain on a
|
||||
/// segment running into an outage or sloping to/from an Uncertain sample; else the earlier sample's — the SDK's
|
||||
/// <c>GetSimpleBound</c>); time before the first sample is Bad; at the end of the data the last region runs to the
|
||||
/// end of its own interval (Part 13; the SDK gives it 1 ms) and later intervals are <c>BadNoData</c>; an interval
|
||||
/// ending before the first sample ⇒ <c>BadNoData</c>. Status Good; any value kind.</description></item>
|
||||
/// <item><term>Interpolative</term><description>The interpolated-bounding signal's value at each interval start —
|
||||
/// for a reverse read, at <c>End − i·interval</c> (Part 13 anchors a reverse read at its later time), emitted
|
||||
/// latest first.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <b>Status (time-weighted four)</b> — the SDK <c>AggregateCalculator.GetTimeBasedStatusCode</c> rule with
|
||||
/// TreatUncertainAsBad = false, so Uncertain time (Uncertain samples, a value held up to an outage, stepped
|
||||
/// extrapolation past the last sample) counts as Good time: every instant of the interval non-Bad ⇒
|
||||
/// <c>Good</c>; no non-Bad time but some Bad (outage) time ⇒ <c>Bad</c>; no data at all ⇒ <c>BadNoData</c>;
|
||||
/// otherwise (PercentDataGood = PercentDataBad = 100) <c>UncertainDataSubNormal</c>. Time before the first sample
|
||||
/// counts as Bad (no data) time. A usable String/DateTime sample contributing to the interval ⇒
|
||||
/// <c>BadAggregateInvalidInputs</c>. Every status carries the Calculated bit, plus the Partial bit on a non-Bad
|
||||
/// result of the truncated interval or of the interval the data starts inside (the first sample fed lies strictly
|
||||
/// within it — forward reads only, as in the SDK, #677); the timestamp is the interval's <see cref="BucketPlan.BucketTimestamp"/> (its start forward, its
|
||||
/// later edge reverse); the value is a
|
||||
/// <see cref="double"/> (Boolean = 0/1, Int64 as double).
|
||||
/// <b>Status (TimeAverage / Total, #680)</b> — Part 13 ("Uncertain if Bad values are skipped or Uncertain values are
|
||||
/// used") and the SDK's <c>AverageAggregateCalculator</c>: no defined value in the interval ⇒ <c>BadNoData</c>; any
|
||||
/// part of it on a bridged segment with an Uncertain end (or Uncertain start when stepped), bridged across a Bad
|
||||
/// sample, extrapolated past the last usable sample, or not covered at all ⇒ <c>UncertainDataSubNormal</c> (so the
|
||||
/// latest interval of a live tag is Uncertain, and an interval wholly inside an outage has the bridged value,
|
||||
/// Uncertain); else <c>Good</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Status (TimeAverage2 / Total2)</b> — the SDK <c>AggregateCalculator.GetTimeBasedStatusCode</c> rule with
|
||||
/// TreatUncertainAsBad = false, so Uncertain time (Uncertain samples, a value held up to an outage) counts as Good
|
||||
/// time, over the interval cut at the end of the data: after the end of the data ⇒ <c>BadNoData</c>; the data
|
||||
/// ending exactly on the interval start ⇒ the last sample's value (Total2 0) with its status; every covered instant
|
||||
/// non-Bad ⇒ <c>Good</c>; no non-Bad time but some Bad (outage) time ⇒ <c>Bad</c> (Part 13's PercentDataBad rule;
|
||||
/// the SDK answers <c>BadNoData</c>); no data at all ⇒ <c>BadNoData</c>; otherwise (PercentDataGood = PercentDataBad
|
||||
/// = 100) <c>UncertainDataSubNormal</c>. Time before the first sample counts as Bad (no data) time.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For the four, a usable String/DateTime sample contributing to the interval ⇒ <c>BadAggregateInvalidInputs</c>.
|
||||
/// Every status of this family but Interpolative's carries the Calculated bit, plus — on a non-Bad result only — the
|
||||
/// Partial bit on: the truncated interval (Part 13 §5.3.3.2 Case 3; the SDK does not set it — deliberate); the
|
||||
/// interval the data starts inside in the read direction (forward: the first sample fed lies strictly within it,
|
||||
/// #677; reverse: the latest sample fed lies strictly within it, nothing after — the SDK's <c>m_startOfData</c>,
|
||||
/// keyed on the first value it is fed, #680); and, forward, the end-of-data interval of TimeAverage2 / Total2 /
|
||||
/// PercentGood / PercentBad (the SDK's <c>m_endOfData</c>, which it skips for an extrapolated TimeAverage / Total).
|
||||
/// The timestamp is the interval's <see cref="BucketPlan.BucketTimestamp"/> (its start forward, its later edge
|
||||
/// reverse); the value is a <see cref="double"/> (Boolean = 0/1, Int64 as double).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Status (Interpolative).</b> A usable sample exactly at the point ⇒ its value with its own status
|
||||
/// (raw, no historian bit). Otherwise no usable sample at or before ⇒ <c>BadNoData</c>; else the interpolated
|
||||
/// value with the Interpolated bit and the status of the simple-bounding signal at that instant: <c>Good</c> on
|
||||
/// a Good segment, <c>UncertainDataSubNormal</c> on an Uncertain one, inside an outage (interpolated across it)
|
||||
/// or past the last sample (extrapolated).
|
||||
/// or past the last sample (extrapolated). A reverse read returns the raw sample at a point exactly on a sample
|
||||
/// too — the SDK's reverse answer (Interpolated, from the segment beyond) differs from its own forward one; that
|
||||
/// asymmetry is the SDK's defect, deliberately not copied.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class TimeWeightedAggregator
|
||||
@@ -108,13 +136,19 @@ public sealed class TimeWeightedAggregator
|
||||
// Interpolated-bounding (bridged) signal, per bucket.
|
||||
private readonly double[] _bridgedArea;
|
||||
private readonly long[] _bridgedTicks;
|
||||
// A bridged segment touching the bucket was non-Good: an Uncertain end, a Bad sample skipped, or extrapolated (#680).
|
||||
private readonly bool[] _bridgedNonGood;
|
||||
private readonly bool[] _invalid;
|
||||
// PercentGood / PercentBad (#680): stepped status regions, per bucket.
|
||||
private readonly long[] _goodRegionTicks;
|
||||
private readonly long[] _badRegionTicks;
|
||||
// Interpolative, per point (index = ascending time; see PointTicks).
|
||||
private readonly DataValueSnapshot?[] _points;
|
||||
|
||||
private StoredSample? _prevAny;
|
||||
private StoredSample? _prevUsable;
|
||||
private DateTime? _firstSampleUtc; // the earliest sample fed (any quality): where the data starts
|
||||
private long? _dataEndTicks; // the last sample fed, when usable and nothing at/after the end followed
|
||||
private long _nextPoint; // next point (ascending time) not yet classified
|
||||
private readonly List<PendingPoint> _pending = []; // points awaiting the next usable sample
|
||||
private bool _finished;
|
||||
@@ -146,7 +180,10 @@ public sealed class TimeWeightedAggregator
|
||||
_badTicks = new long[n];
|
||||
_bridgedArea = new double[n];
|
||||
_bridgedTicks = new long[n];
|
||||
_bridgedNonGood = new bool[n];
|
||||
_invalid = new bool[n];
|
||||
_goodRegionTicks = new long[n];
|
||||
_badRegionTicks = new long[n];
|
||||
_points = new DataValueSnapshot?[n];
|
||||
}
|
||||
|
||||
@@ -172,7 +209,11 @@ public sealed class TimeWeightedAggregator
|
||||
if (_finished) throw new InvalidOperationException("The aggregation is already finished.");
|
||||
var t = s.TimeUtc.Ticks;
|
||||
if (_prevAny is { } last && t <= last.TimeUtc.Ticks) return;
|
||||
_firstSampleUtc ??= s.TimeUtc;
|
||||
if (_firstSampleUtc is null)
|
||||
{
|
||||
_firstSampleUtc = s.TimeUtc;
|
||||
AddRegion(_badRegionTicks, _start, t); // PercentBad: the time before the first sample is Bad (no data)
|
||||
}
|
||||
|
||||
// Interpolative: classify every bucket start before this sample by the simple-bounding signal there.
|
||||
ClassifyPointsBefore(t);
|
||||
@@ -188,6 +229,7 @@ public sealed class TimeWeightedAggregator
|
||||
{
|
||||
AddBad(bad, t);
|
||||
}
|
||||
if (_prevAny is { } r) AddStatusRegion(r, s, t);
|
||||
|
||||
if (!s.IsUsable) DegradePending(); // the segment the pending points sit on runs into an outage
|
||||
|
||||
@@ -196,7 +238,10 @@ public sealed class TimeWeightedAggregator
|
||||
if (_prevUsable is { } pu)
|
||||
{
|
||||
var sloped = IsSloped(pu, s);
|
||||
AddBridged(pu, t, sloped ? s.Value.Number : pu.Value.Number);
|
||||
// Part 13: Uncertain when a Bad value was skipped or an Uncertain value is used (the end sample only
|
||||
// contributes its value to a sloped segment).
|
||||
var nonGood = !pu.IsGood || (sloped && !s.IsGood) || _prevAny is { IsUsable: false };
|
||||
AddBridged(pu, t, sloped ? s.Value.Number : pu.Value.Number, nonGood);
|
||||
}
|
||||
ResolvePending(_prevUsable, s);
|
||||
_prevUsable = s;
|
||||
@@ -215,11 +260,21 @@ public sealed class TimeWeightedAggregator
|
||||
_finished = true;
|
||||
ClassifyPointsBefore(long.MaxValue);
|
||||
if (_prevAny is { IsUsable: true } p && p.TimeUtc.Ticks < _end)
|
||||
AddRaw(p, _end, p.Value.Number); // stepped extrapolation
|
||||
{
|
||||
// No sample at or after the window end: the data ENDS here (Part 13 "After End of Data"). The simple-
|
||||
// bounding signal is not extrapolated (TimeAverage2/Total2 cover up to it; later intervals BadNoData), and
|
||||
// the status region it starts runs to the end of its own interval only (PercentGood/PercentBad).
|
||||
_dataEndTicks = p.TimeUtc.Ticks;
|
||||
if (p.TimeUtc.Ticks >= _start && _plan.BucketCount > 0)
|
||||
AddRegion(RegionTicks(p), p.TimeUtc.Ticks, BucketEnd(FirstBucketAt(p.TimeUtc.Ticks)));
|
||||
}
|
||||
else if (_prevAny is { } bad && bad.TimeUtc.Ticks < _end)
|
||||
AddBad(bad, _end); // the outage runs to the window end
|
||||
{
|
||||
AddBad(bad, _end); // an outage marker last: the outage runs to the window end
|
||||
AddRegion(_badRegionTicks, bad.TimeUtc.Ticks, _end);
|
||||
}
|
||||
if (_prevUsable is { } pu && pu.TimeUtc.Ticks < _end)
|
||||
AddBridged(pu, _end, pu.Value.Number);
|
||||
AddBridged(pu, _end, pu.Value.Number, nonGood: true); // stepped extrapolation ⇒ Uncertain
|
||||
ResolvePending(_prevUsable, next: null);
|
||||
}
|
||||
|
||||
@@ -239,30 +294,77 @@ public sealed class TimeWeightedAggregator
|
||||
: new DataValueSnapshot(null, HistorianStatusCodes.BadNoData, PointTime(i), now);
|
||||
|
||||
var stamp = _plan.BucketTimestamp(i);
|
||||
var duration = BucketEnd(i) - BucketStartTicks(i);
|
||||
// Partial: the truncated interval (the latest forward, the earliest reverse), or the one the data starts inside
|
||||
// (no bounding value before it — the SDK's m_startOfData rule, #677), which the SDK applies to forward reads only.
|
||||
var partial = _plan.IsTruncated(i) || (!_plan.Reversed && _plan.DataStartsInside(i, _firstSampleUtc));
|
||||
DataValueSnapshot Calc(object? value, uint status)
|
||||
var lo = BucketStartTicks(i);
|
||||
var hi = BucketEnd(i);
|
||||
var duration = hi - lo;
|
||||
// Partial: the truncated interval (the latest forward, the earliest reverse — Part 13 §5.3.3.2 Case 3, which the
|
||||
// SDK does not implement), or the one the data starts inside IN THE READ DIRECTION (no bounding value on that
|
||||
// side — the SDK's m_startOfData rule, #677): forward, the earliest sample strictly inside; reverse, the latest
|
||||
// sample strictly inside (nothing after it — the SDK keys m_startOfData on the first value it is fed, #680).
|
||||
// Forward, the end-of-data interval (the latest sample inside it, nothing after) is Partial too — never on an
|
||||
// extrapolated TimeAverage/Total (the SDK's m_endOfData rule, #680). Never on a Bad result.
|
||||
var latestInside = _plan.Reversed && _prevAny is { } latest
|
||||
&& latest.TimeUtc.Ticks > lo && latest.TimeUtc.Ticks < hi;
|
||||
var partial = _plan.IsTruncated(i) || latestInside
|
||||
|| (!_plan.Reversed && _plan.DataStartsInside(i, _firstSampleUtc));
|
||||
var endOfData = !_plan.Reversed && _dataEndTicks is { } de && de >= lo && de < hi;
|
||||
DataValueSnapshot Calc(object? value, uint status, bool endPartial = false)
|
||||
{
|
||||
var code = HistorianInfoBits.WithCalculated(status);
|
||||
if (partial && !StatusBits.IsBad(status)) code |= HistorianInfoBits.HistorianPartial;
|
||||
if ((partial || endPartial) && !StatusBits.IsBad(status)) code |= HistorianInfoBits.HistorianPartial;
|
||||
return new DataValueSnapshot(value, code, stamp, now);
|
||||
}
|
||||
|
||||
if (_invalid[i]) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
|
||||
if (_usableTicks[i] == 0)
|
||||
return _badTicks[i] > 0 ? Calc(null, HistorianStatusCodes.Bad) : AggregateEvaluator.NoData(stamp, now);
|
||||
|
||||
var status = _usableTicks[i] == duration ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
|
||||
return _aggregate switch
|
||||
switch (_aggregate)
|
||||
{
|
||||
HistoryAggregateType.TimeAverage => Calc(_bridgedArea[i] / _bridgedTicks[i], status),
|
||||
HistoryAggregateType.Total => Calc(_bridgedArea[i] / _bridgedTicks[i] * (duration / TicksPerSecond), status),
|
||||
HistoryAggregateType.TimeAverage2 => Calc(_rawArea[i] / _usableTicks[i], status),
|
||||
HistoryAggregateType.Total2 => Calc(_rawArea[i] / TicksPerSecond, status),
|
||||
_ => throw new HistoryAggregateNotSupportedException(_aggregate),
|
||||
};
|
||||
case HistoryAggregateType.TimeAverage or HistoryAggregateType.Total:
|
||||
{
|
||||
// Interpolated bounding: Uncertain when any part of the interval was bridged across a Bad value, used an
|
||||
// Uncertain value, was extrapolated, or is not covered at all (Part 13; the SDK's AverageAggregateCalculator).
|
||||
if (_invalid[i]) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
|
||||
if (_bridgedTicks[i] == 0) return AggregateEvaluator.NoData(stamp, now);
|
||||
var status = _bridgedNonGood[i] || _bridgedTicks[i] < duration
|
||||
? HistorianStatusCodes.UncertainDataSubNormal
|
||||
: HistorianStatusCodes.Good;
|
||||
return _aggregate == HistoryAggregateType.TimeAverage
|
||||
? Calc(_bridgedArea[i] / _bridgedTicks[i], status)
|
||||
: Calc(_bridgedArea[i] / TicksPerSecond, status); // ∫ over the covered part only (Part 13 partial width)
|
||||
}
|
||||
case HistoryAggregateType.TimeAverage2 or HistoryAggregateType.Total2:
|
||||
{
|
||||
if (_invalid[i]) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
|
||||
if (_dataEndTicks is { } dataEnd)
|
||||
{
|
||||
// After the end of the data: nothing to aggregate (Part 13 "After End of Data: Bad_NoData").
|
||||
if (lo > dataEnd) return AggregateEvaluator.NoData(stamp, now);
|
||||
if (lo == dataEnd && _prevAny is { } last)
|
||||
{
|
||||
// The data ends exactly on this interval's start: nothing covered; the value at that instant.
|
||||
if (!IsNumeric(last)) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
|
||||
var st = last.IsGood ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
|
||||
return Calc(_aggregate == HistoryAggregateType.TimeAverage2 ? last.Value.Number : 0d, st, endOfData);
|
||||
}
|
||||
duration = Math.Min(hi, dataEnd) - lo;
|
||||
}
|
||||
if (_usableTicks[i] == 0)
|
||||
return _badTicks[i] > 0 ? Calc(null, HistorianStatusCodes.Bad) : AggregateEvaluator.NoData(stamp, now);
|
||||
var status = _usableTicks[i] == duration ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
|
||||
return _aggregate == HistoryAggregateType.TimeAverage2
|
||||
? Calc(_rawArea[i] / _usableTicks[i], status, endOfData)
|
||||
: Calc(_rawArea[i] / TicksPerSecond, status, endOfData);
|
||||
}
|
||||
case HistoryAggregateType.PercentGood or HistoryAggregateType.PercentBad:
|
||||
{
|
||||
// No data in or around the interval (it ends before the first sample — the SDK's out-of-range test — or
|
||||
// starts after the end of the data) ⇒ BadNoData; otherwise the share of the interval's duration.
|
||||
if (_firstSampleUtc is not { } first || hi < first.Ticks || (_dataEndTicks is { } de2 && lo > de2))
|
||||
return AggregateEvaluator.NoData(stamp, now);
|
||||
var ticks = _aggregate == HistoryAggregateType.PercentGood ? _goodRegionTicks[i] : _badRegionTicks[i];
|
||||
return Calc(100d * ticks / duration, HistorianStatusCodes.Good, endOfData);
|
||||
}
|
||||
default:
|
||||
throw new HistoryAggregateNotSupportedException(_aggregate);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------ Segments
|
||||
@@ -305,13 +407,68 @@ public sealed class TimeWeightedAggregator
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBridged(StoredSample from, long toTicks, double toValue) =>
|
||||
private void AddBridged(StoredSample from, long toTicks, double toValue, bool nonGood) =>
|
||||
Distribute(from, toTicks, toValue, (i, area, ticks) =>
|
||||
{
|
||||
_bridgedArea[i] += area;
|
||||
_bridgedTicks[i] += ticks;
|
||||
if (nonGood) _bridgedNonGood[i] = true;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------ PercentGood / PercentBad
|
||||
|
||||
/// <summary>The region array a stepped region of <paramref name="s"/>'s status counts toward (null: Uncertain, neither).</summary>
|
||||
private long[]? RegionTicks(StoredSample s) =>
|
||||
s.IsGood ? _goodRegionTicks : !s.IsUsable ? _badRegionTicks : null;
|
||||
|
||||
/// <summary>
|
||||
/// The status regions between sample <paramref name="p"/> and the next sample <paramref name="s"/> (at
|
||||
/// <paramref name="t"/>). Inside the interval <paramref name="p"/> lies in, the region has <paramref name="p"/>'s
|
||||
/// status (stepped, Part 13 §5.4.3.31-34). In a later interval the region starts at that interval's simple bounding
|
||||
/// value, whose status is the simple-bounding signal's: Bad after a Bad <paramref name="p"/>; Uncertain when the
|
||||
/// segment runs into an outage or a sloped one has a non-Good end; else <paramref name="p"/>'s (the SDK's
|
||||
/// <c>GetSimpleBound</c>).
|
||||
/// </summary>
|
||||
private void AddStatusRegion(StoredSample p, StoredSample s, long t)
|
||||
{
|
||||
var lo = Math.Max(p.TimeUtc.Ticks, _start);
|
||||
var hi = Math.Min(t, _end);
|
||||
if (lo >= hi || _plan.BucketCount == 0) return;
|
||||
long[]? bound;
|
||||
if (!p.IsUsable) bound = _badRegionTicks;
|
||||
else if (!s.IsUsable) bound = null; // held into an outage ⇒ Uncertain
|
||||
else if (IsSloped(p, s)) bound = p.IsGood && s.IsGood ? _goodRegionTicks : null;
|
||||
else bound = RegionTicks(p);
|
||||
var own = RegionTicks(p);
|
||||
for (var i = FirstBucketAt(lo); i < _plan.BucketCount; i++)
|
||||
{
|
||||
var bs = BucketStartTicks(i);
|
||||
if (bs >= hi) break;
|
||||
var a = Math.Max(lo, bs);
|
||||
var b = Math.Min(hi, BucketEnd(i));
|
||||
if (a >= b) continue;
|
||||
var target = p.TimeUtc.Ticks >= bs ? own : bound;
|
||||
if (target is not null) target[i] += b - a;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds <c>[from, to)</c>, clipped to the window, to <paramref name="target"/> per bucket.</summary>
|
||||
private void AddRegion(long[]? target, long from, long to)
|
||||
{
|
||||
if (target is null) return;
|
||||
var lo = Math.Max(from, _start);
|
||||
var hi = Math.Min(to, _end);
|
||||
if (lo >= hi || _plan.BucketCount == 0) return;
|
||||
for (var i = FirstBucketAt(lo); i < _plan.BucketCount; i++)
|
||||
{
|
||||
var bs = BucketStartTicks(i);
|
||||
if (bs >= hi) break;
|
||||
var a = Math.Max(lo, bs);
|
||||
var b = Math.Min(hi, BucketEnd(i));
|
||||
if (a < b) target[i] += b - a;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the linear segment <c>[from.t, toTicks)</c> (value <c>from.v → toValue</c>) at the bucket edges, clipped to
|
||||
/// the window, and hands each bucket its trapezoid area (value·ticks) and duration. A non-numeric start sample
|
||||
|
||||
Reference in New Issue
Block a user