Files
romanett 78e66f3473 Aggregates: TreatUncertainAsBad never applies to bounding values; DurationGood/Bad count Good/Bad regions only (#4702)
# Description

Server-side aggregate fixes that follow three answers from the OPC
Foundation on Mantis:
- [11462](https://mantis.opcfoundation.org/view.php?id=11462)
(~0025840): TreatUncertainAsBad does not apply to bounding values.
- [11425](https://mantis.opcfoundation.org/view.php?id=11425)
(~0025847): DurationGood counts only Good regions and DurationBad only
Bad regions, and TreatUncertainAsBad does not apply.
- [11426](https://mantis.opcfoundation.org/view.php?id=11426)
(~0025852): Minimum, Maximum, the ActualTime variants and Range look at
Uncertain values as if they are Good.

The principle in 11425 and 11426 is that the aggregate definition wins
over TreatUncertainAsBad. The CTT-side defects are filed as
[11487](https://mantis.opcfoundation.org/view.php?id=11487).

### Bounding values (`AggregateCalculator`,
`StartEndAggregateCalculator`)
- Simple (§3.1.9) and Interpolated (§3.1.8) Bounding Values are formed
from **non-Bad** raw values (`IsBoundCandidate`), whatever
TreatUncertainAsBad is.
- An Uncertain raw value before or after the timestamp makes the bound
`UncertainDataSubNormal`.
- A raw value exactly at the timestamp is the bound and keeps its own
status.
- Only a **Bad** value after a Simple bound reverts to stepped
interpolation.
- StartBound/EndBound keep an Uncertain bound instead of `Bad_NoData`
(Tables 76/77).
- WorstQuality2 now includes an Uncertain start bound, as §5.4.3.36
requires ("the server shall ignore the TreatUncertainAsBad for this
aggregate").

### How aggregates use the values (unchanged rule, two gaps closed)
TreatUncertainAsBad still governs how an aggregate uses the values
inside the interval, including an Uncertain bound.
- `GetRegionsInValueSet` now also converts an Uncertain **first** region
to Bad for Simple-bound aggregates. TimeAverage2/Total2 therefore omit
it like any other Uncertain region (§5.4.3.7, Table 55 "Bound
Uncertain").
- TimeAverage/Total (Interpolated bounds) still use an Uncertain bound
and report Uncertain (Table 54).
- TimeAverage2/Total2 return the PercentTime status `Bad` instead of
`Bad_NoData` for an interval that has data but only omitted data
(§5.4.3.2.1).
- Minimum2/Maximum2 are unchanged: an Uncertain bound is a candidate
only when TreatUncertainAsBad is false (§5.4.3.15).

### NumberOfTransitions (`CountAggregateCalculator`)
§5.4.3.24 speaks only of "non-Bad" values, and Table 72 says "Bound
Uncertain: Use as value". So:
- Uncertain values are counted and can be the previous value, whatever
TreatUncertainAsBad is.
- The previous value is the nearest non-Bad value before the interval
(the early bound).

This reverses an earlier change that made NumberOfTransitions honour
TreatUncertainAsBad.

### Minimum, Maximum, MinimumActualTime, MaximumActualTime, Range
(`MinMaxAggregateCalculator`)
Per the 11426 answer:
- Every non-Bad value is a candidate, whatever TreatUncertainAsBad is.
- An Uncertain extremum is returned with `UncertainDataSubNormal` plus
Calculated. ActualTime keeps the timestamp of the first such value.
- Otherwise the status is Good unless the interval contains Bad values
(then `UncertainDataSubNormal`); it is `BadNoData` if all values are
Bad.
- Range is max − min over the non-Bad values. It is Uncertain if either
extremum is Uncertain or Bad values exist.

Before this change the server ignored an Uncertain value beyond the Good
extremum and returned the Good one with an Uncertain status.
Minimum2/Maximum2/Range2 are unchanged, because §5.4.3.15 ties them to
TreatUncertainAsBad explicitly.

### DurationGood/DurationBad/PercentGood/PercentBad
(`StatusAggregateCalculator`)
- A region is Good only if its data point is Good, and Bad only if it is
Bad.
- Uncertain counts for neither (§5.4.3.31–34).
- The first region still takes the status of the raw value at or before
the start.

### Tests
- New `Aggregates/BoundingValueTreatUncertainAsBadTests`: DeltaBounds,
StartBound/EndBound, TimeAverage2/Total2, Interpolative,
NumberOfTransitions (forward and reverse),
DurationGood/Bad/PercentGood/Bad.
- New Min/Max test for Bad-only and Bad-next-to-Good intervals.
- Updated tests that encoded the old interpretation:
  - Interpolative with an Uncertain raw value at the start: 2 tests.
  - Duration/Percent Good/Bad: 2 tests.
  - NumberOfTransitions with TreatUncertainAsBad: 2 tests.
- Minimum/Maximum/Range with an Uncertain extremum or only Uncertain
values: 2 tests in Server.Tests, 1 in History.Tests.
- `ProcessedReadUsesAdvertisedNodeAggregateDefaultsAsync`: it now probes
the TreatUncertainAsBad default with Count, because PercentGood no
longer depends on it.

### CTT verification
CTT 1.05.513 with the patched scripts (c3674ff, which encode the
11462/11425/11426 answers), Aggregates part (p06):

| Server | Errors | Warnings | Pass | NotImpl |
| --- | --- | --- | --- | --- |
| master fa06c66db | 185 | 1 | 1078 | 3 |
| this branch (854769a0c) | **0** | 2 | 1262 | 3 |

The 185 errors on master break down as:
- Bounds: DeltaBounds 9, StartBound 12, EndBound 12, WorstQuality2 11,
TimeAverage2 5, Total2 5, Interpolative 1.
- Duration*/Percent*: DurationBad 18, PercentBad 18, DurationGood 4,
PercentGood 4.
- Minimum/Maximum/MinimumActualTime/MaximumActualTime/Range: 70.
- NumberOfTransitions: 16.

The 2 warnings on this branch are ActivateSession response-delay
warnings in `initialize.js`, caused by machine load.

Alarms & Conditions (p18) failed intermittently on PR builds: in the
Refresh2, Shelving and Exclusive/Non-Exclusive Level CUs, subscriptions
were lost or filtered events were missing. The same clean build of
854769a0c failed once (3 errors) and passed once (0 errors). The PR with
`src/Opc.Ua.Server/Aggregates` reverted passed, and master passes too.
These failures are A&C flakiness on a heavily loaded shared machine and
are not caused by this PR, whose code runs only in HistoryRead and
aggregate-filter MonitoredItems.

Full 23-part CTT run against this branch (854769a0c, patched scripts
c3674ff/c341918): **1 error in total**, GDS Application Directory
`019.js`, a known CTT native-stack defect (Mantis 11409) that also fails
on master. Aggregates (p06) and Alarms & Conditions (p18) have 0 errors,
and no test case went from Pass to Error compared with master.

### Reviewer notes
- §3.1.7 says *"In all cases the TreatUncertainAsBad (see 4.2.1.2) flag
is used to determine whether Uncertain values are Bad or non-Bad."* This
branch follows the answer to 11462, which states that the flag does not
apply to bounding values. This is documented in `plans/ctt-issues.md`
(U4).
- TimeAverage2/Total2 still apply TreatUncertainAsBad to an Uncertain
start bound when they use it: Table 55 says "Bound Uncertain: Treated as
any other Uncertain value", and the definition omits non-Good data. This
includes a start bound that is Uncertain only because the raw value
after it is Bad.
- Not in scope: `CastToOriginalType` uses `Convert.ToInt32(double)`
(banker's rounding), whereas Part 4 §7.7.3 says to add 0.5 and truncate.
The two differ only at .5 and for negative values.

## Related Issues

- Mantis 11462, 11425 (answers implemented), 11487 (CTT side)

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
(Aggregate/Historian tests of Opc.Ua.Server.Tests and
Opc.Ua.History.Tests on net10.0; net48 build only)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:49:13 +02:00
..