#region License Information /* * This file is part of SimSharp which is licensed under the MIT license. * See the LICENSE file in the project root for more information. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace SimSharp { public sealed class Report { [Flags] public enum Measures { Min = 1, Max = 2, Sum = 4, Mean = 8, StdDev = 16, Last = 32, All = 63 } private enum UpdateType { Auto = 0, Manual = 1, Periodic = 2, Summary = 3 } private Simulation environment; private List keys; private UpdateType updateType; private TimeSpan periodicUpdateInterval; private bool withHeaders; private string separator; private bool useDoubleTime; private DateTime lastUpdate; private double[] lastFigures; private bool firstUpdate; private bool headerWritten; /// /// Gets or sets the output target. /// /// /// This is not thread-safe and must be set only when the simulation is not running. /// public TextWriter Output { get; set; } private Report() { keys = new List(); updateType = UpdateType.Auto; periodicUpdateInterval = TimeSpan.Zero; environment = null; Output = Console.Out; separator = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator; useDoubleTime = false; withHeaders = true; } private void Initialize() { environment.RunStarted += SimulationOnRunStarted; environment.RunFinished += SimulationOnRunFinished; if (updateType == UpdateType.Auto) { foreach (var k in keys) k.Statistics.Updated += StatisticsOnUpdated; } } private void SimulationOnRunStarted(object sender, EventArgs e) { var cols = keys.Sum(x => x.TotalMeasures); lastFigures = new double[cols]; lastUpdate = environment.Now; firstUpdate = true; headerWritten = false; if (updateType == UpdateType.Periodic) { environment.Process(PeriodicUpdateProcess()); } else if (updateType == UpdateType.Auto) { DoUpdate(); } } private void SimulationOnRunFinished(object sender, EventArgs e) { if (updateType == UpdateType.Periodic || updateType == UpdateType.Summary) DoUpdate(); if (updateType == UpdateType.Summary && withHeaders) WriteHeader(); WriteLastFigures(); Output.Flush(); } private void DoUpdate() { if (updateType != UpdateType.Summary && !firstUpdate && environment.Now > lastUpdate) { // values are written only when simulation time has actually passed to prevent 0-time updates if (!headerWritten && withHeaders) { WriteHeader(); headerWritten = true; } WriteLastFigures(); } lastUpdate = environment.Now; var col = 0; foreach (var fig in keys) { if ((fig.Measure & Measures.Min) == Measures.Min) lastFigures[col++] = fig.Statistics.Min; if ((fig.Measure & Measures.Max) == Measures.Max) lastFigures[col++] = fig.Statistics.Max; if ((fig.Measure & Measures.Sum) == Measures.Sum) lastFigures[col++] = fig.Statistics.Sum; if ((fig.Measure & Measures.Mean) == Measures.Mean) lastFigures[col++] = fig.Statistics.Mean; if ((fig.Measure & Measures.StdDev) == Measures.StdDev) lastFigures[col++] = fig.Statistics.StdDev; if ((fig.Measure & Measures.Last) == Measures.Last) lastFigures[col++] = fig.Statistics.Last; } firstUpdate = false; } /// /// Writes the header manually to . This may be useful if /// headers are not automatically added. /// public void WriteHeader() { Output.Write("Time"); foreach (var fig in keys) { if ((fig.Measure & Measures.Min) == Measures.Min) { Output.Write(separator); Output.Write(fig.Name + ".Min"); } if ((fig.Measure & Measures.Max) == Measures.Max) { Output.Write(separator); Output.Write(fig.Name + ".Max"); } if ((fig.Measure & Measures.Sum) == Measures.Sum) { Output.Write(separator); Output.Write(fig.Name + ".Sum"); } if ((fig.Measure & Measures.Mean) == Measures.Mean) { Output.Write(separator); Output.Write(fig.Name + ".Mean"); } if ((fig.Measure & Measures.StdDev) == Measures.StdDev) { Output.Write(separator); Output.Write(fig.Name + ".StdDev"); } if ((fig.Measure & Measures.Last) == Measures.Last) { Output.Write(separator); Output.Write(fig.Name + ".Last"); } } Output.WriteLine(); } private void WriteLastFigures() { var col = 0; if (useDoubleTime) Output.Write(environment.ToDouble(lastUpdate - environment.StartDate)); else Output.Write(lastUpdate.ToString()); foreach (var fig in keys) { if ((fig.Measure & Measures.Min) == Measures.Min) { Output.Write(separator); Output.Write(lastFigures[col++]); } if ((fig.Measure & Measures.Max) == Measures.Max) { Output.Write(separator); Output.Write(lastFigures[col++]); } if ((fig.Measure & Measures.Sum) == Measures.Sum) { Output.Write(separator); Output.Write(lastFigures[col++]); } if ((fig.Measure & Measures.Mean) == Measures.Mean) { Output.Write(separator); Output.Write(lastFigures[col++]); } if ((fig.Measure & Measures.StdDev) == Measures.StdDev) { Output.Write(separator); Output.Write(lastFigures[col++]); } if ((fig.Measure & Measures.Last) == Measures.Last) { Output.Write(separator); Output.Write(lastFigures[col++]); } } Output.WriteLine(); } /// /// Performs a manual update. It must only be called when manual update is chosen. /// /// Thrown when calling this function in another update mode. public void Update() { if (updateType != UpdateType.Manual) throw new InvalidOperationException("Update may only be called in manual update mode."); DoUpdate(); } private void StatisticsOnUpdated(object sender, EventArgs e) { DoUpdate(); } private IEnumerable PeriodicUpdateProcess() { while (true) { DoUpdate(); yield return environment.Timeout(periodicUpdateInterval); } } /// /// Creates a new report builder for configuring the report. A report can be generated by /// calling the builder's method. /// /// The simulation environment for which a report should be generated. /// The builder instance that is used to configure a new report. public static Builder CreateBuilder(Simulation env) { return new Builder(env); } /// /// The Builder class is used to configure and create a new report. /// public class Builder { private Report instance; /// /// Creates a new builder for generating a report. /// /// The simulation environment for which the report should be generated. public Builder(Simulation env) { instance = new Report() { environment = env }; } /// /// Adds a new indicator to the report. /// /// The name of the indicator for which the statistic is created. /// The statistics instance for the indicator that contains the values. /// The measure(s) that should be reported. /// Thrown when is null or empty, /// or when is not valid. /// Thrown when is null. /// This builder instance. public Builder Add(string name, INumericMonitor statistics, Measures measure = Measures.All) { if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name must be a non-empty string", "name"); if (statistics == null) throw new ArgumentNullException("statistics"); if (measure == 0 || measure > Measures.All) throw new ArgumentException("No measures have been selected.", "measure"); instance.keys.Add(new Key { Name = name, Statistics = statistics, Measure = measure, TotalMeasures = CountSetBits((int)measure) }); return this; } /// /// In automatic updating mode (default), the report will listen to the /// event and perform an update whenever /// any of its statistics is updated. /// /// Auto update is mutually exclusive to the other update modes. /// Auto update with headers is the default. /// Whether the headers should be output before the first values are printed. /// This builder instance. public Builder SetAutoUpdate(bool withHeaders = true) { instance.withHeaders = withHeaders; instance.updateType = UpdateType.Auto; return this; } /// /// In manual updating mode, the method needs to be called /// manually in order to record the current state. /// /// Manual update is mutually exclusive to the other update modes. /// Whether the headers should be output before the first values are printed. /// This builder instance. public Builder SetManualUpdate(bool withHeaders = true) { instance.withHeaders = withHeaders; instance.updateType = UpdateType.Manual; return this; } /// /// In periodic updating mode, the report will create a process that periodically /// triggers the update. The process will be created upon calling . /// /// Periodic update is mutually exclusive to the other update modes. /// Thrown when is less or equal than TimeSpan.Zero. /// The interval after which an update occurs. /// Whether the headers should be output before the first values are printed. /// This builder instance. public Builder SetPeriodicUpdate(TimeSpan interval, bool withHeaders = true) { if (interval <= TimeSpan.Zero) throw new ArgumentException("Interval must be > 0", "interval"); instance.periodicUpdateInterval = interval; instance.withHeaders = withHeaders; instance.updateType = UpdateType.Periodic; return this; } /// /// In periodic updating mode, the report will create a process that periodically /// triggers the update. The process will be created upon calling . /// /// Periodic update is mutually exclusive to the other update modes. /// Thrown when is less or equal than 0. /// The interval after which an update occurs. /// Whether the headers should be output before the first values are printed. /// This builder instance. public Builder SetPeriodicUpdateD(double interval, bool withHeaders = true) { if (interval <= 0) throw new ArgumentException("Interval must be > 0", "interval"); instance.periodicUpdateInterval = instance.environment.ToTimeSpan(interval); instance.withHeaders = withHeaders; instance.updateType = UpdateType.Periodic; return this; } /// /// In final update mode, the report will only update when the simulation terminates correctly. /// This is useful for generating a summary of the results. /// /// Final update is mutually exclusive to the other update modes. /// Whether the headers should be output together with the summary at the end. /// This builder instance. public Builder SetFinalUpdate(bool withHeaders = true) { instance.withHeaders = withHeaders; instance.updateType = UpdateType.Summary; return this; } /// /// Whether to output the time column in DateTime format or as double (D-API). /// /// Whether the time should be output as double. /// This builder instance. public Builder SetTimeAPI(bool useDApi = true) { instance.useDoubleTime = useDApi; return this; } /// /// Redirects the output of the report to another target. /// By default it is configured to use stdout. /// /// Thrown when is null. /// The target to which the output should be directed. /// This builder instance. public Builder SetOutput(TextWriter output) { this.instance.Output = output ?? throw new ArgumentNullException("output"); return this; } /// /// Sets the separator for the indicators' values. /// /// The string that separates the values. /// This builder instance. public Builder SetSeparator(string seperator) { if (seperator == null) seperator = string.Empty; this.instance.separator = seperator; return this; } /// /// Creates and initializes the report. After calling Build(), this builder instance /// is reset and can be reused to create a new report. /// /// Thrown when no indicators have been added. /// The created report instance. public Report Build() { if (!instance.keys.Any()) throw new InvalidOperationException("Nothing to build: No indicators have been added to the Builder."); var result = instance; instance = new Report(); result.Initialize(); return result; } private static readonly int[] numToBits = new int[16] { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; private static int CountSetBits(int num) { return numToBits[num & 0xf] + numToBits[(num >> 4) & 0xf] + numToBits[(num >> 8) & 0xf] + numToBits[(num >> 16) & 0xf] + numToBits[(num >> 20) & 0xf] + numToBits[(num >> 24) & 0xf] + numToBits[(num >> 28) & 0xf]; } } private class Key { public string Name { get; set; } public INumericMonitor Statistics { get; set; } public Measures Measure { get; set; } public int TotalMeasures { get; set; } } } }