fix(historian): #684 review — Uncertain non-numeric samples do not refuse the Good-value aggregates; real-SQL Uncertain-above-max test

- Average/Minimum/Maximum/Range/*ActualTime refuse (BadAggregateInvalidInputs) only a GOOD
  String/DateTime sample (new GoodNonNumericCount / good_non_numeric_count column); an Uncertain
  one is skipped and makes the min/max family Uncertain, matching the SDK (Average skips it,
  ComputeMinMax flags the uncastable value). Delta keeps its usable-sample refusal.
- ProcessedConformance: an Uncertain value above the Good maximum (pins UncMax/unc_max through
  the real SQL) and an Uncertain String through the real SQL.
- The_uncertain_dataset_is_not_vacuous also proves the min/max STATUS discrimination.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-10-12 09:54:51 -04:00
parent a11ab0dc08
commit c0d6bacd9c
13 changed files with 240 additions and 27 deletions
@@ -74,7 +74,11 @@ public static class AggregateEvaluator
return Calc(r.Good, r.Good == r.Total ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal);
}
if (r.Usable == 0) return NoData(bucketStart, now);
if (r.NonNumeric > 0) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
// #684 review: Delta reads usable picks, so any usable String/DateTime sample refuses it. The Good-value aggregates
// refuse only a GOOD non-numeric sample — an Uncertain one is not part of their value set (it only degrades the
// status, as the SDK's skipped/uncastable value does).
if ((aggregate == HistoryAggregateType.Delta ? r.NonNumeric : r.GoodNonNumericOrAll) > 0)
return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
switch (aggregate)
{
@@ -122,7 +126,8 @@ public static class AggregateEvaluator
/// #684 — the Part 13 status of Minimum / Maximum / Range / *ActualTime (Tables 57–62): <c>Uncertain_DataSubNormal</c>
/// when a Bad sample exists in the interval, or when an Uncertain value is less than the minimum Good value
/// (<paramref name="low"/>) / greater than the maximum Good value (<paramref name="high"/>); else Good. An Uncertain
/// value inside the Good range leaves it Good.
/// value inside the Good range leaves it Good. An Uncertain String/DateTime sample (no order against the Good values)
/// also makes it Uncertain — the SDK's <c>ComputeMinMax</c> flags a value it cannot cast exactly that way.
/// </summary>
/// <remarks>
/// The SDK's <c>ComputeMinMax</c> (1.5.378) applies only the Bad half: it collects the Uncertain extremes and never
@@ -131,7 +136,8 @@ public static class AggregateEvaluator
/// Uncertain value EQUAL to the Good extreme does not degrade it.
/// </remarks>
private static uint MinMaxStatus(BucketRow r, double? goodMin, double? goodMax, bool low, bool high) =>
r.Bad > 0 || (low && r.UncertainMin < goodMin) || (high && r.UncertainMax > goodMax)
r.Bad > 0 || r.NonNumeric > r.GoodNonNumericOrAll
|| (low && r.UncertainMin < goodMin) || (high && r.UncertainMax > goodMax)
? HistorianStatusCodes.UncertainDataSubNormal
: HistorianStatusCodes.Good;
@@ -32,9 +32,20 @@ public sealed class BucketRow
/// <summary>Bad (not usable) samples.</summary>
public int Bad { get; set; }
/// <summary>Usable String / DateTime samples.</summary>
/// <summary>Usable String / DateTime samples (Delta's refusal check).</summary>
public int NonNumeric { get; set; }
/// <summary>
/// <b>Good</b> String / DateTime samples — the refusal check of the Good-value aggregates (Average / Minimum / Maximum /
/// Range / *ActualTime, #684 review): an Uncertain non-numeric sample is not part of their value set. Filled by the
/// Simple and ActualTime queries; null (a row that does not carry it) falls back to <see cref="NonNumeric"/>, the
/// pre-#684 (stricter) check.
/// </summary>
public int? GoodNonNumeric { get; set; }
/// <summary><see cref="GoodNonNumeric"/>, or <see cref="NonNumeric"/> when the row does not carry it.</summary>
internal int GoodNonNumericOrAll => GoodNonNumeric ?? NonNumeric;
/// <summary>Average of the Good numeric values (null when none — #684).</summary>
public double? Avg { get; set; }