using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; using Opc.Ua; using Opc.Ua.Server; if (args.Length != 1) throw new ArgumentException("Pass a fresh output JSON path."); if (File.Exists(args[0])) throw new IOException("Refusing to overwrite existing baseline evidence."); var epoch = new DateTime(2026, 10, 1, 8, 0, 0, DateTimeKind.Utc).AddTicks(1234567); var telemetry = DefaultTelemetry.Create(_ => { }); var offsets = new double[] { -10, 0, 5, 15, 25, 35, 50, 80 }; var data = new Dictionary { ["double"] = offsets.Select((s, i) => new Sample(s, "Double", (10 + i * 7 % 13).ToString(CultureInfo.InvariantCulture), StatusCodes.Good)).ToArray(), ["uncertain"] = offsets.Select((s, i) => new Sample(s, i == 4 ? "Null" : "Double", i == 4 ? null : i == 2 ? "100" : (10 + i * 7 % 13).ToString(CultureInfo.InvariantCulture), i == 2 ? StatusCodes.UncertainDataSubNormal : i == 4 ? StatusCodes.BadNoCommunication : StatusCodes.Good)).ToArray(), ["integer"] = offsets.Select((s, i) => new Sample(s, "Int64", (i * 7 - 10).ToString(CultureInfo.InvariantCulture), i == 3 ? StatusCodes.UncertainDataSubNormal : StatusCodes.Good)).ToArray(), ["boolean"] = offsets.Select((s, i) => new Sample(s, "Boolean", (i % 2 == 0).ToString(), i == 2 ? StatusCodes.UncertainDataSubNormal : StatusCodes.Good)).ToArray(), ["sparse"] = [new(-10, "Double", "10", StatusCodes.Good), new(7, "Double", "20", StatusCodes.Good), new(18, "Null", null, StatusCodes.BadNoCommunication), new(45, "Double", "30", StatusCodes.Good)], ["bad"] = offsets.Select(s => new Sample(s, "Null", null, StatusCodes.BadNoCommunication)).ToArray(), ["empty"] = [], }; var names = new string[] { "AggregateFunction_Interpolative", "AggregateFunction_Average", "AggregateFunction_TimeAverage", "AggregateFunction_TimeAverage2", "AggregateFunction_Total", "AggregateFunction_Total2", "AggregateFunction_Minimum", "AggregateFunction_Maximum", "AggregateFunction_MinimumActualTime", "AggregateFunction_MaximumActualTime", "AggregateFunction_Range", "AggregateFunction_Minimum2", "AggregateFunction_Maximum2", "AggregateFunction_MinimumActualTime2", "AggregateFunction_MaximumActualTime2", "AggregateFunction_Range2", "AggregateFunction_Count", "AggregateFunction_AnnotationCount", "AggregateFunction_DurationInStateZero", "AggregateFunction_DurationInStateNonZero", "AggregateFunction_NumberOfTransitions", "AggregateFunction_Start", "AggregateFunction_End", "AggregateFunction_Delta", "AggregateFunction_StartBound", "AggregateFunction_EndBound", "AggregateFunction_DeltaBounds", "AggregateFunction_DurationGood", "AggregateFunction_DurationBad", "AggregateFunction_PercentGood", "AggregateFunction_PercentBad", "AggregateFunction_WorstQuality", "AggregateFunction_WorstQuality2", "AggregateFunction_StandardDeviationPopulation", "AggregateFunction_VariancePopulation", "AggregateFunction_StandardDeviationSample", "AggregateFunction_VarianceSample", }; var vectors = new List(); // 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 { ["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 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(); var accepted = new List(); string? error = null; try { var samples = data[dataset].OrderBy(x => x.Seconds); foreach (var sample in reverse ? samples.Reverse() : samples) { var time = epoch.AddSeconds(sample.Seconds); 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, 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++) { var value = calculator.GetProcessedValue(partial); if (value is null) return; output.Add(new(partial, value.Value?.GetType().Name, Format(value.Value), value.StatusCode.Code, value.SourceTimestamp.Ticks, value.ServerTimestamp.Ticks, value.SourcePicoseconds, value.ServerPicoseconds)); } throw new InvalidOperationException("Baseline calculator did not terminate."); } } static object? Payload(Sample sample) => sample.Type switch { "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, }; static string? Format(object? value) => value switch { null => null, double number => number.ToString("R", CultureInfo.InvariantCulture), float number => number.ToString("R", CultureInfo.InvariantCulture), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), _ => value.ToString(), }; 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 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; } }