test: expand original SDK aggregate edge vectors

This commit is contained in:
Joseph Doherty
2026-11-10 10:54:02 -05:00
parent aac7c88417
commit 8c422b9ccc
2 changed files with 186 additions and 12 deletions
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Opc.Ua;
using Opc.Ua.Server;
@@ -60,33 +61,137 @@ var names = new string[]
"AggregateFunction_VarianceSample",
};
var vectors = new List<Vector>();
// Preserve the original seven-dataset grid, input ordering and expected values exactly.
foreach (var name in names)
foreach (var dataset in data)
foreach (var reverse in new[] { false, true })
foreach (var stepped in new[] { false, true })
foreach (var uncertainAsBad in new[] { false, true })
foreach (var incremental in new[] { false, true })
Run(name, dataset.Key, reverse, stepped, uncertainAsBad, incremental);
var edges = new Dictionary<string, Sample[]>
{
["edge-sbyte"] = Typed("SByte", ["-128", "-127", "-1", "0", "1", "126", "127", "2"]),
["edge-byte"] = Typed("Byte", ["0", "1", "127", "128", "254", "255", "3", "2"]),
["edge-int16"] = Typed("Int16", ["-32768", "-32767", "-1", "0", "1", "32766", "32767", "2"]),
["edge-uint16"] = Typed("UInt16", ["0", "1", "32767", "32768", "65534", "65535", "3", "2"]),
["edge-int32"] = Typed("Int32", ["-2147483648", "-2147483647", "-1", "0", "1", "2147483646", "2147483647", "2"]),
["edge-uint32"] = Typed("UInt32", ["0", "1", "2147483647", "2147483648", "4294967294", "4294967295", "3", "2"]),
["edge-int64"] = Typed("Int64", ["-9223372036854775808", "-9007199254740993", "-1", "0", "1", "9007199254740993", "9223372036854775806", "9223372036854775807"]),
["edge-uint64"] = Typed("UInt64", ["0", "1", "9007199254740991", "9007199254740993", "9223372036854775807", "9223372036854775808", "18446744073709551614", "18446744073709551615"]),
["edge-float"] = Typed("Single", ["-3.4028235E+38", "1E-45", "-0", "1.5", "3.4028235E+38", "NaN", "Infinity", "-Infinity"]),
["edge-double"] = Typed("Double", ["-1.5", "-0", "0.5", "1.5", "NaN", "Infinity", "-Infinity", "1.7976931348623157E+308"]),
// Interpolative/Delta casts back to the earlier sample's type: fractional, negative,
// overflowing and nonfinite Double values must exercise the original SDK conversion.
["edge-mixed-conversion"] =
[
new(-10, "Byte", "0", StatusCodes.Good), new(0, "Byte", "1", StatusCodes.Good),
new(5, "Double", "2.5", StatusCodes.Good), new(15, "SByte", "-128", StatusCodes.Good),
new(25, "Double", "255.5", StatusCodes.Good), new(35, "Single", "3.4028235E+38", StatusCodes.Good),
new(50, "Double", "Infinity", StatusCodes.Good), new(80, "UInt64", "18446744073709551615", StatusCodes.Good),
],
};
foreach (var dataset in edges) data.Add(dataset.Key, dataset.Value);
foreach (var name in names)
foreach (var dataset in edges)
foreach (var reverse in new[] { false, true })
foreach (var incremental in new[] { false, true })
Run(name, dataset.Key, reverse, false, false, incremental, Settings(false));
// Ties include an identical source timestamp+picoseconds pair; input order is stable.
// Distinct server ticks and picoseconds must survive raw-result selection independently.
data.Add("edge-duplicate-picoseconds",
[
Pico(-10, "1", 3, 19, 7), Pico(0, "2", 1, 23, 11), Pico(0, "3", 9999, 29, 13),
Pico(5, "4", 7, 31, 17), Pico(5, "5", 7, 37, 19), Pico(20, "6", 2, 41, 23),
Pico(20, "7", 9000, 43, 29), Pico(40, "8", 5, 47, 31), Pico(80, "9", 11, 53, 37),
]);
foreach (var name in names)
foreach (var reverse in new[] { false, true })
foreach (var incremental in new[] { false, true })
Run(name, "edge-duplicate-picoseconds", reverse, false, false, incremental, Settings(false));
var configurations = new[]
{
new AggregateSettings(false, 0, 0, false, false),
new AggregateSettings(false, 1, 99, true, false),
new AggregateSettings(true, 50, 50, false, false),
new AggregateSettings(true, 100, 0, true, false),
};
var configuredNames = new[]
{
"AggregateFunction_Interpolative", "AggregateFunction_TimeAverage", "AggregateFunction_TimeAverage2",
"AggregateFunction_Total", "AggregateFunction_Total2", "AggregateFunction_PercentGood", "AggregateFunction_PercentBad",
};
foreach (var name in configuredNames)
foreach (var dataset in new[] { "uncertain", "sparse" })
foreach (var config in configurations)
foreach (var reverse in new[] { false, true })
foreach (var incremental in new[] { false, true })
Run(name, dataset, reverse, false, config.TreatUncertainAsBad, incremental, config);
var assembly = typeof(Aggregators).Assembly.Location;
var assemblySha256 = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(assembly)));
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(args[0]))!);
// One vector per line keeps golden changes reviewable without pretty-printing every value.
using (var writer = new StreamWriter(new FileStream(args[0], FileMode.CreateNew, FileAccess.Write)))
{
writer.WriteLine("{");
writer.WriteLine($" \"sdk\":\"1.5.378.182\",\"assemblySha256\":\"{assemblySha256}\",");
writer.WriteLine($" \"epochTicks\":{epoch.Ticks},\"intervalMilliseconds\":20000,");
writer.WriteLine($" \"data\":{JsonSerializer.Serialize(data)},");
writer.WriteLine(" \"vectors\":[");
for (var i = 0; i < vectors.Count; i++)
writer.WriteLine(" " + JsonSerializer.Serialize(vectors[i]) + (i + 1 < vectors.Count ? "," : ""));
writer.WriteLine(" ]");
writer.WriteLine("}");
}
Console.WriteLine($"Generated {vectors.Count} vectors ({vectors.Count - 4144} focused additions) for {names.Length} aggregate IDs; baseline assembly SHA256 {assemblySha256}.");
Sample[] Typed(string type, string[] values) => offsets.Zip(values, (s, value) => new Sample(s, type, value, StatusCodes.Good)).ToArray();
static Sample Pico(double seconds, string value, ushort source, ushort server, long serverOffset) =>
new(seconds, "Int32", value, StatusCodes.Good)
{ SourcePicoseconds = source, ServerPicoseconds = server, ServerOffsetTicks = serverOffset };
static AggregateSettings Settings(bool uncertainAsBad) => new(uncertainAsBad, 100, 100, false, false);
void Run(string name, string dataset, bool reverse, bool stepped, bool uncertainAsBad, bool incremental,
AggregateSettings? settings = null)
{
var id = (NodeId)typeof(ObjectIds).GetField(name)!.GetValue(null)!;
var start = epoch.AddSeconds(reverse ? 70 : 0);
var end = epoch.AddSeconds(reverse ? 0 : 70);
var config = new AggregateConfiguration { TreatUncertainAsBad = uncertainAsBad, PercentDataBad = 100, PercentDataGood = 100, UseSlopedExtrapolation = false, UseServerCapabilitiesDefaults = false };
var cfg = settings ?? Settings(uncertainAsBad);
var config = new AggregateConfiguration
{
TreatUncertainAsBad = cfg.TreatUncertainAsBad, PercentDataBad = cfg.PercentDataBad,
PercentDataGood = cfg.PercentDataGood, UseSlopedExtrapolation = cfg.UseSlopedExtrapolation,
UseServerCapabilitiesDefaults = cfg.UseServerCapabilitiesDefaults,
};
var calculator = Aggregators.CreateStandardCalculator(id, start, end, 20_000, stepped, config, telemetry);
var output = new List<Value>();
var accepted = new List<bool>();
string? error = null;
try
{
var samples = dataset.Value.OrderBy(x => x.Seconds);
var samples = data[dataset].OrderBy(x => x.Seconds);
foreach (var sample in reverse ? samples.Reverse() : samples)
{
var time = epoch.AddSeconds(sample.Seconds);
calculator.QueueRawValue(new DataValue(new Variant(Payload(sample)!), sample.Status, time, time));
var value = new DataValue(new Variant(Payload(sample)!), sample.Status, time,
time.AddTicks(sample.ServerOffsetTicks ?? 0))
{
SourcePicoseconds = sample.SourcePicoseconds ?? 0,
ServerPicoseconds = sample.ServerPicoseconds ?? 0,
};
accepted.Add(calculator.QueueRawValue(value));
if (incremental) Drain(false);
}
Drain(true);
}
catch (Exception ex) { error = ex.GetType().FullName; }
vectors.Add(new(name, dataset.Key, reverse, stepped, uncertainAsBad, incremental, output.ToArray(), error));
vectors.Add(new(name, dataset, reverse, stepped, uncertainAsBad, incremental, output.ToArray(), error)
{ Configuration = settings, QueueAccepted = settings is null ? null : accepted.ToArray() });
void Drain(bool partial)
{
for (var guard = 0; guard < 100; guard++)
@@ -98,16 +203,19 @@ foreach (var incremental in new[] { false, true })
throw new InvalidOperationException("Baseline calculator did not terminate.");
}
}
var assembly = typeof(Aggregators).Assembly.Location;
var baseline = new { sdk = "1.5.378.182", assemblySha256 = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(assembly))), epochTicks = epoch.Ticks, intervalMilliseconds = 20_000, data, vectors };
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(args[0]))!);
File.WriteAllText(args[0], JsonSerializer.Serialize(baseline, new JsonSerializerOptions { WriteIndented = true }) + "\n");
Console.WriteLine($"Generated {vectors.Count} vectors for {names.Length} aggregate IDs; baseline assembly SHA256 {baseline.assemblySha256}.");
static object? Payload(Sample sample) => sample.Type switch
{
"Double" => double.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"SByte" => sbyte.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Byte" => byte.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Int16" => short.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"UInt16" => ushort.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Int32" => int.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"UInt32" => uint.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Int64" => long.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"UInt64" => ulong.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Single" => float.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Double" => double.Parse(sample.Payload!, CultureInfo.InvariantCulture),
"Boolean" => bool.Parse(sample.Payload!),
_ => null,
};
@@ -119,6 +227,16 @@ static string? Format(object? value) => value switch
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString(),
};
record Sample(double Seconds, string Type, string? Payload, uint Status);
record Sample(double Seconds, string Type, string? Payload, uint Status)
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ushort? SourcePicoseconds { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ushort? ServerPicoseconds { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? ServerOffsetTicks { get; init; }
}
record Value(bool PartialRequested, string? Type, string? Payload, uint Status, long SourceTicks, long ServerTicks, ushort SourcePicoseconds, ushort ServerPicoseconds);
record Vector(string Aggregate, string DataSet, bool Reverse, bool Stepped, bool UncertainAsBad, bool Incremental, Value[] Output, string? Exception);
record AggregateSettings(bool TreatUncertainAsBad, byte PercentDataBad, byte PercentDataGood, bool UseSlopedExtrapolation, bool UseServerCapabilitiesDefaults);
record Vector(string Aggregate, string DataSet, bool Reverse, bool Stepped, bool UncertainAsBad, bool Incremental, Value[] Output, string? Exception)
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AggregateSettings? Configuration { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool[]? QueueAccepted { get; init; }
}