using System.Globalization; namespace AquaSynth.Dsl; public static class PrimitiveTimelineFactExtractor { public static IReadOnlyList Extract(IReadOnlyList timeline) { if (timeline.Count == 0) { return []; } var facts = new List { Fact( "timeline_blocks", "timeline", "block_count", timeline.Select(sample => sample.Block).Distinct().Count(), "blocks", timeline.Min(sample => sample.Block), timeline.Max(sample => sample.Block), "Observed primitive timeline block count.") }; AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_min_opening", "normalized", Min); AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_mean_opening", "normalized", Mean); AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_closed_blocks", "blocks", ClosedBlocks); AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "reservoir", "contact_reservoir_peak", "pressure", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "released_flow", "contact_release_peak", "flow", MaxAbs); AddPeakBlockFacts(facts, timeline, "contact:", "released_flow", "contact_release_peak_block"); AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "load_pressure", "source_load_pressure_peak", "pressure", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "flow", "source_flow_peak", "flow", MaxAbs); AddPeakBlockFacts(facts, timeline, "source:", "flow", "source_flow_peak_block"); AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "admittance", "branch_admittance_peak", "admittance", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "exchanged_flow", "branch_exchanged_flow_peak", "flow", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "boundary_flow", "radiation_boundary_flow_peak", "flow", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_peak", "amplitude", MaxAbs); AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_mean_abs", "amplitude", MeanAbs); AddPeakBlockFacts(facts, timeline, "radiation:", "output", "radiation_output_peak_block"); AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "area", "path_mean_area", "area", Mean); AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "delay_samples", "path_mean_delay_samples", "samples", Mean); AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_in", "path_energy_in_sum", "energy", Sum); AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_out", "path_energy_out_sum", "energy", Sum); AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "passivity_ratio", "path_passivity_max", "ratio", Max); return facts .Where(fact => float.IsFinite(fact.Value)) .OrderBy(fact => fact.Name, StringComparer.Ordinal) .ThenBy(fact => fact.Primitive, StringComparer.Ordinal) .ThenBy(fact => fact.BlockStart) .ToArray(); } private static void AddSignalFacts( List facts, IReadOnlyList timeline, string primitivePrefix, string signal, string name, string unit, Func, float> reducer) { foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase)) { var samples = group.ToArray(); if (samples.Length == 0) { continue; } var value = reducer(samples); facts.Add(Fact( name, group.Key, signal, value, unit, samples.Min(sample => sample.Block), samples.Max(sample => sample.Block), Summary(name, group.Key, signal, value, unit))); } } private static void AddPeakBlockFacts( List facts, IReadOnlyList timeline, string primitivePrefix, string signal, string name) { foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase)) { var peak = group .OrderByDescending(sample => Math.Abs(sample.Value)) .ThenBy(sample => sample.Block) .FirstOrDefault(); if (peak is null) { continue; } facts.Add(Fact( name, peak.Primitive, signal, peak.Block, "block", peak.Block, peak.Block, Summary(name, peak.Primitive, signal, peak.Block, "block"))); } } private static IEnumerable Matching( IReadOnlyList timeline, string primitivePrefix, string signal) => timeline.Where(sample => sample.Primitive.StartsWith(primitivePrefix, StringComparison.OrdinalIgnoreCase) && sample.Signal.Equals(signal, StringComparison.OrdinalIgnoreCase) && float.IsFinite(sample.Value)); private static PrimitiveTimelineFact Fact( string name, string primitive, string signal, float value, string unit, int blockStart, int blockEnd, string summary) => new(name, primitive, signal, value, unit, blockStart, blockEnd, summary); private static string Summary(string name, string primitive, string signal, float value, string unit) => $"{name} {primitive} {signal} {value.ToString("0.######", CultureInfo.InvariantCulture)} {unit}"; private static float Min(IReadOnlyList samples) => samples.Min(sample => sample.Value); private static float Max(IReadOnlyList samples) => samples.Max(sample => sample.Value); private static float Mean(IReadOnlyList samples) => samples.Average(sample => sample.Value); private static float Sum(IReadOnlyList samples) => samples.Sum(sample => sample.Value); private static float MaxAbs(IReadOnlyList samples) => samples.Max(sample => Math.Abs(sample.Value)); private static float MeanAbs(IReadOnlyList samples) => samples.Average(sample => Math.Abs(sample.Value)); private static float ClosedBlocks(IReadOnlyList samples) => samples.Count(sample => sample.Value <= 0.2f); }