[18023] | 1 | #region License Information |
---|
| 2 | /* |
---|
| 3 | * This file is part of SimSharp which is licensed under the MIT license. |
---|
| 4 | * See the LICENSE file in the project root for more information. |
---|
| 5 | */ |
---|
| 6 | #endregion |
---|
| 7 | |
---|
| 8 | using System; |
---|
| 9 | using System.Collections.Generic; |
---|
| 10 | using System.IO; |
---|
| 11 | using System.Linq; |
---|
| 12 | |
---|
| 13 | namespace SimSharp { |
---|
| 14 | public sealed class Report { |
---|
| 15 | [Flags] |
---|
| 16 | public enum Measures { Min = 1, Max = 2, Sum = 4, Mean = 8, StdDev = 16, Last = 32, All = 63 } |
---|
| 17 | private enum UpdateType { Auto = 0, Manual = 1, Periodic = 2, Summary = 3 } |
---|
| 18 | |
---|
| 19 | private Simulation environment; |
---|
| 20 | private List<Key> keys; |
---|
| 21 | private UpdateType updateType; |
---|
| 22 | private TimeSpan periodicUpdateInterval; |
---|
| 23 | private bool withHeaders; |
---|
| 24 | private string separator; |
---|
| 25 | private bool useDoubleTime; |
---|
| 26 | |
---|
| 27 | private DateTime lastUpdate; |
---|
| 28 | private double[] lastFigures; |
---|
| 29 | private bool firstUpdate; |
---|
| 30 | private bool headerWritten; |
---|
| 31 | |
---|
| 32 | /// <summary> |
---|
| 33 | /// Gets or sets the output target. |
---|
| 34 | /// </summary> |
---|
| 35 | /// <remarks> |
---|
| 36 | /// This is not thread-safe and must be set only when the simulation is not running. |
---|
| 37 | /// </remarks> |
---|
| 38 | public TextWriter Output { get; set; } |
---|
| 39 | |
---|
| 40 | private Report() { |
---|
| 41 | keys = new List<Key>(); |
---|
| 42 | updateType = UpdateType.Auto; |
---|
| 43 | periodicUpdateInterval = TimeSpan.Zero; |
---|
| 44 | environment = null; |
---|
| 45 | Output = Console.Out; |
---|
| 46 | separator = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator; |
---|
| 47 | useDoubleTime = false; |
---|
| 48 | withHeaders = true; |
---|
| 49 | } |
---|
| 50 | |
---|
| 51 | private void Initialize() { |
---|
| 52 | environment.RunStarted += SimulationOnRunStarted; |
---|
| 53 | environment.RunFinished += SimulationOnRunFinished; |
---|
| 54 | if (updateType == UpdateType.Auto) { |
---|
| 55 | foreach (var k in keys) k.Statistics.Updated += StatisticsOnUpdated; |
---|
| 56 | } |
---|
| 57 | } |
---|
| 58 | |
---|
| 59 | private void SimulationOnRunStarted(object sender, EventArgs e) { |
---|
| 60 | var cols = keys.Sum(x => x.TotalMeasures); |
---|
| 61 | lastFigures = new double[cols]; |
---|
| 62 | lastUpdate = environment.Now; |
---|
| 63 | firstUpdate = true; |
---|
| 64 | headerWritten = false; |
---|
| 65 | |
---|
| 66 | if (updateType == UpdateType.Periodic) { |
---|
| 67 | environment.Process(PeriodicUpdateProcess()); |
---|
| 68 | } else if (updateType == UpdateType.Auto) { |
---|
| 69 | DoUpdate(); |
---|
| 70 | } |
---|
| 71 | } |
---|
| 72 | |
---|
| 73 | private void SimulationOnRunFinished(object sender, EventArgs e) { |
---|
| 74 | if (updateType == UpdateType.Periodic || updateType == UpdateType.Summary) DoUpdate(); |
---|
| 75 | if (updateType == UpdateType.Summary && withHeaders) WriteHeader(); |
---|
| 76 | WriteLastFigures(); |
---|
| 77 | Output.Flush(); |
---|
| 78 | } |
---|
| 79 | |
---|
| 80 | private void DoUpdate() { |
---|
| 81 | if (updateType != UpdateType.Summary && !firstUpdate && environment.Now > lastUpdate) { |
---|
| 82 | // values are written only when simulation time has actually passed to prevent 0-time updates |
---|
| 83 | if (!headerWritten && withHeaders) { |
---|
| 84 | WriteHeader(); |
---|
| 85 | headerWritten = true; |
---|
| 86 | } |
---|
| 87 | WriteLastFigures(); |
---|
| 88 | } |
---|
| 89 | lastUpdate = environment.Now; |
---|
| 90 | var col = 0; |
---|
| 91 | foreach (var fig in keys) { |
---|
| 92 | if ((fig.Measure & Measures.Min) == Measures.Min) |
---|
| 93 | lastFigures[col++] = fig.Statistics.Min; |
---|
| 94 | if ((fig.Measure & Measures.Max) == Measures.Max) |
---|
| 95 | lastFigures[col++] = fig.Statistics.Max; |
---|
| 96 | if ((fig.Measure & Measures.Sum) == Measures.Sum) |
---|
| 97 | lastFigures[col++] = fig.Statistics.Sum; |
---|
| 98 | if ((fig.Measure & Measures.Mean) == Measures.Mean) |
---|
| 99 | lastFigures[col++] = fig.Statistics.Mean; |
---|
| 100 | if ((fig.Measure & Measures.StdDev) == Measures.StdDev) |
---|
| 101 | lastFigures[col++] = fig.Statistics.StdDev; |
---|
| 102 | if ((fig.Measure & Measures.Last) == Measures.Last) |
---|
| 103 | lastFigures[col++] = fig.Statistics.Last; |
---|
| 104 | } |
---|
| 105 | firstUpdate = false; |
---|
| 106 | } |
---|
| 107 | |
---|
| 108 | /// <summary> |
---|
| 109 | /// Writes the header manually to <see cref="Output"/>. This may be useful if |
---|
| 110 | /// headers are not automatically added. |
---|
| 111 | /// </summary> |
---|
| 112 | public void WriteHeader() { |
---|
| 113 | Output.Write("Time"); |
---|
| 114 | foreach (var fig in keys) { |
---|
| 115 | if ((fig.Measure & Measures.Min) == Measures.Min) { |
---|
| 116 | Output.Write(separator); |
---|
| 117 | Output.Write(fig.Name + ".Min"); |
---|
| 118 | } |
---|
| 119 | if ((fig.Measure & Measures.Max) == Measures.Max) { |
---|
| 120 | Output.Write(separator); |
---|
| 121 | Output.Write(fig.Name + ".Max"); |
---|
| 122 | } |
---|
| 123 | if ((fig.Measure & Measures.Sum) == Measures.Sum) { |
---|
| 124 | Output.Write(separator); |
---|
| 125 | Output.Write(fig.Name + ".Sum"); |
---|
| 126 | } |
---|
| 127 | if ((fig.Measure & Measures.Mean) == Measures.Mean) { |
---|
| 128 | Output.Write(separator); |
---|
| 129 | Output.Write(fig.Name + ".Mean"); |
---|
| 130 | } |
---|
| 131 | if ((fig.Measure & Measures.StdDev) == Measures.StdDev) { |
---|
| 132 | Output.Write(separator); |
---|
| 133 | Output.Write(fig.Name + ".StdDev"); |
---|
| 134 | } |
---|
| 135 | if ((fig.Measure & Measures.Last) == Measures.Last) { |
---|
| 136 | Output.Write(separator); |
---|
| 137 | Output.Write(fig.Name + ".Last"); |
---|
| 138 | } |
---|
| 139 | } |
---|
| 140 | Output.WriteLine(); |
---|
| 141 | } |
---|
| 142 | |
---|
| 143 | private void WriteLastFigures() { |
---|
| 144 | var col = 0; |
---|
| 145 | if (useDoubleTime) Output.Write(environment.ToDouble(lastUpdate - environment.StartDate)); |
---|
| 146 | else Output.Write(lastUpdate.ToString()); |
---|
| 147 | foreach (var fig in keys) { |
---|
| 148 | if ((fig.Measure & Measures.Min) == Measures.Min) { |
---|
| 149 | Output.Write(separator); |
---|
| 150 | Output.Write(lastFigures[col++]); |
---|
| 151 | } |
---|
| 152 | if ((fig.Measure & Measures.Max) == Measures.Max) { |
---|
| 153 | Output.Write(separator); |
---|
| 154 | Output.Write(lastFigures[col++]); |
---|
| 155 | } |
---|
| 156 | if ((fig.Measure & Measures.Sum) == Measures.Sum) { |
---|
| 157 | Output.Write(separator); |
---|
| 158 | Output.Write(lastFigures[col++]); |
---|
| 159 | } |
---|
| 160 | if ((fig.Measure & Measures.Mean) == Measures.Mean) { |
---|
| 161 | Output.Write(separator); |
---|
| 162 | Output.Write(lastFigures[col++]); |
---|
| 163 | } |
---|
| 164 | if ((fig.Measure & Measures.StdDev) == Measures.StdDev) { |
---|
| 165 | Output.Write(separator); |
---|
| 166 | Output.Write(lastFigures[col++]); |
---|
| 167 | } |
---|
| 168 | if ((fig.Measure & Measures.Last) == Measures.Last) { |
---|
| 169 | Output.Write(separator); |
---|
| 170 | Output.Write(lastFigures[col++]); |
---|
| 171 | } |
---|
| 172 | } |
---|
| 173 | Output.WriteLine(); |
---|
| 174 | } |
---|
| 175 | |
---|
| 176 | /// <summary> |
---|
| 177 | /// Performs a manual update. It must only be called when manual update is chosen. |
---|
| 178 | /// </summary> |
---|
| 179 | /// <exception cref="InvalidOperationException">Thrown when calling this function in another update mode.</exception> |
---|
| 180 | public void Update() { |
---|
| 181 | if (updateType != UpdateType.Manual) throw new InvalidOperationException("Update may only be called in manual update mode."); |
---|
| 182 | DoUpdate(); |
---|
| 183 | } |
---|
| 184 | |
---|
| 185 | private void StatisticsOnUpdated(object sender, EventArgs e) { |
---|
| 186 | DoUpdate(); |
---|
| 187 | } |
---|
| 188 | |
---|
| 189 | private IEnumerable<Event> PeriodicUpdateProcess() { |
---|
| 190 | while (true) { |
---|
| 191 | DoUpdate(); |
---|
| 192 | yield return environment.Timeout(periodicUpdateInterval); |
---|
| 193 | } |
---|
| 194 | } |
---|
| 195 | |
---|
| 196 | /// <summary> |
---|
| 197 | /// Creates a new report builder for configuring the report. A report can be generated by |
---|
| 198 | /// calling the builder's <see cref="Builder.Build"/> method. |
---|
| 199 | /// </summary> |
---|
| 200 | /// <param name="env">The simulation environment for which a report should be generated.</param> |
---|
| 201 | /// <returns>The builder instance that is used to configure a new report.</returns> |
---|
| 202 | public static Builder CreateBuilder(Simulation env) { |
---|
| 203 | return new Builder(env); |
---|
| 204 | } |
---|
| 205 | |
---|
| 206 | /// <summary> |
---|
| 207 | /// The Builder class is used to configure and create a new report. |
---|
| 208 | /// </summary> |
---|
| 209 | public class Builder { |
---|
| 210 | private Report instance; |
---|
| 211 | /// <summary> |
---|
| 212 | /// Creates a new builder for generating a report. |
---|
| 213 | /// </summary> |
---|
| 214 | /// <param name="env">The simulation environment for which the report should be generated.</param> |
---|
| 215 | public Builder(Simulation env) { |
---|
| 216 | instance = new Report() { environment = env }; |
---|
| 217 | } |
---|
| 218 | |
---|
| 219 | /// <summary> |
---|
| 220 | /// Adds a new indicator to the report. |
---|
| 221 | /// </summary> |
---|
| 222 | /// <param name="name">The name of the indicator for which the statistic is created.</param> |
---|
| 223 | /// <param name="statistics">The statistics instance for the indicator that contains the values.</param> |
---|
| 224 | /// <param name="measure">The measure(s) that should be reported.</param> |
---|
| 225 | /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null or empty, |
---|
| 226 | /// or when <paramref name="measure"/> is not valid.</exception> |
---|
| 227 | /// <exception cref="ArgumentNullException">Thrown when <paramref name="statistics"/> is null.</exception> |
---|
| 228 | /// <returns>This builder instance.</returns> |
---|
| 229 | public Builder Add(string name, INumericMonitor statistics, Measures measure = Measures.All) { |
---|
| 230 | if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name must be a non-empty string", "name"); |
---|
| 231 | if (statistics == null) throw new ArgumentNullException("statistics"); |
---|
| 232 | if (measure == 0 || measure > Measures.All) throw new ArgumentException("No measures have been selected.", "measure"); |
---|
| 233 | |
---|
| 234 | instance.keys.Add(new Key { Name = name, Statistics = statistics, Measure = measure, TotalMeasures = CountSetBits((int)measure) }); |
---|
| 235 | return this; |
---|
| 236 | } |
---|
| 237 | |
---|
| 238 | /// <summary> |
---|
| 239 | /// In automatic updating mode (default), the report will listen to the |
---|
| 240 | /// <see cref="IMonitor.Updated"/> event and perform an update whenever |
---|
| 241 | /// any of its statistics is updated. |
---|
| 242 | /// </summary> |
---|
| 243 | /// <remarks>Auto update is mutually exclusive to the other update modes. |
---|
| 244 | /// Auto update with headers is the default.</remarks> |
---|
| 245 | /// <param name="withHeaders">Whether the headers should be output before the first values are printed.</param> |
---|
| 246 | /// <returns>This builder instance.</returns> |
---|
| 247 | public Builder SetAutoUpdate(bool withHeaders = true) { |
---|
| 248 | instance.withHeaders = withHeaders; |
---|
| 249 | instance.updateType = UpdateType.Auto; |
---|
| 250 | return this; |
---|
| 251 | } |
---|
| 252 | |
---|
| 253 | /// <summary> |
---|
| 254 | /// In manual updating mode, the <see cref="Report.Update"/> method needs to be called |
---|
| 255 | /// manually in order to record the current state. |
---|
| 256 | /// </summary> |
---|
| 257 | /// <remarks>Manual update is mutually exclusive to the other update modes.</remarks> |
---|
| 258 | /// <param name="withHeaders">Whether the headers should be output before the first values are printed.</param> |
---|
| 259 | /// <returns>This builder instance.</returns> |
---|
| 260 | public Builder SetManualUpdate(bool withHeaders = true) { |
---|
| 261 | instance.withHeaders = withHeaders; |
---|
| 262 | instance.updateType = UpdateType.Manual; |
---|
| 263 | return this; |
---|
| 264 | } |
---|
| 265 | |
---|
| 266 | /// <summary> |
---|
| 267 | /// In periodic updating mode, the report will create a process that periodically |
---|
| 268 | /// triggers the update. The process will be created upon calling <see cref="Build"/>. |
---|
| 269 | /// </summary> |
---|
| 270 | /// <remarks>Periodic update is mutually exclusive to the other update modes.</remarks> |
---|
| 271 | /// <exception cref="ArgumentException">Thrown when <paramref name="interval"/> is less or equal than TimeSpan.Zero.</exception> |
---|
| 272 | /// <param name="interval">The interval after which an update occurs.</param> |
---|
| 273 | /// <param name="withHeaders">Whether the headers should be output before the first values are printed.</param> |
---|
| 274 | /// <returns>This builder instance.</returns> |
---|
| 275 | public Builder SetPeriodicUpdate(TimeSpan interval, bool withHeaders = true) { |
---|
| 276 | if (interval <= TimeSpan.Zero) throw new ArgumentException("Interval must be > 0", "interval"); |
---|
| 277 | instance.periodicUpdateInterval = interval; |
---|
| 278 | instance.withHeaders = withHeaders; |
---|
| 279 | instance.updateType = UpdateType.Periodic; |
---|
| 280 | return this; |
---|
| 281 | } |
---|
| 282 | |
---|
| 283 | /// <summary> |
---|
| 284 | /// In periodic updating mode, the report will create a process that periodically |
---|
| 285 | /// triggers the update. The process will be created upon calling <see cref="Build"/>. |
---|
| 286 | /// </summary> |
---|
| 287 | /// <remarks>Periodic update is mutually exclusive to the other update modes.</remarks> |
---|
| 288 | /// <exception cref="ArgumentException">Thrown when <paramref name="interval"/> is less or equal than 0.</exception> |
---|
| 289 | /// <param name="interval">The interval after which an update occurs.</param> |
---|
| 290 | /// <param name="withHeaders">Whether the headers should be output before the first values are printed.</param> |
---|
| 291 | /// <returns>This builder instance.</returns> |
---|
| 292 | public Builder SetPeriodicUpdateD(double interval, bool withHeaders = true) { |
---|
| 293 | if (interval <= 0) throw new ArgumentException("Interval must be > 0", "interval"); |
---|
| 294 | instance.periodicUpdateInterval = instance.environment.ToTimeSpan(interval); |
---|
| 295 | instance.withHeaders = withHeaders; |
---|
| 296 | instance.updateType = UpdateType.Periodic; |
---|
| 297 | return this; |
---|
| 298 | } |
---|
| 299 | |
---|
| 300 | /// <summary> |
---|
| 301 | /// In final update mode, the report will only update when the simulation terminates correctly. |
---|
| 302 | /// This is useful for generating a summary of the results. |
---|
| 303 | /// </summary> |
---|
| 304 | /// <remarks>Final update is mutually exclusive to the other update modes.</remarks> |
---|
| 305 | /// <param name="withHeaders">Whether the headers should be output together with the summary at the end.</param> |
---|
| 306 | /// <returns>This builder instance.</returns> |
---|
| 307 | public Builder SetFinalUpdate(bool withHeaders = true) { |
---|
| 308 | instance.withHeaders = withHeaders; |
---|
| 309 | instance.updateType = UpdateType.Summary; |
---|
| 310 | return this; |
---|
| 311 | } |
---|
| 312 | |
---|
| 313 | /// <summary> |
---|
| 314 | /// Whether to output the time column in DateTime format or as double (D-API). |
---|
| 315 | /// </summary> |
---|
| 316 | /// <param name="useDApi">Whether the time should be output as double.</param> |
---|
| 317 | /// <returns>This builder instance.</returns> |
---|
| 318 | public Builder SetTimeAPI(bool useDApi = true) { |
---|
| 319 | instance.useDoubleTime = useDApi; |
---|
| 320 | return this; |
---|
| 321 | } |
---|
| 322 | |
---|
| 323 | /// <summary> |
---|
| 324 | /// Redirects the output of the report to another target. |
---|
| 325 | /// By default it is configured to use stdout. |
---|
| 326 | /// </summary> |
---|
| 327 | /// <exception cref="ArgumentNullException">Thrown when <paramref name="output"/> is null.</exception> |
---|
| 328 | /// <param name="output">The target to which the output should be directed.</param> |
---|
| 329 | /// <returns>This builder instance.</returns> |
---|
| 330 | public Builder SetOutput(TextWriter output) { |
---|
| 331 | this.instance.Output = output ?? throw new ArgumentNullException("output"); |
---|
| 332 | return this; |
---|
| 333 | } |
---|
| 334 | |
---|
| 335 | /// <summary> |
---|
| 336 | /// Sets the separator for the indicators' values. |
---|
| 337 | /// </summary> |
---|
| 338 | /// <param name="seperator">The string that separates the values.</param> |
---|
| 339 | /// <returns>This builder instance.</returns> |
---|
| 340 | public Builder SetSeparator(string seperator) { |
---|
| 341 | if (seperator == null) seperator = string.Empty; |
---|
| 342 | this.instance.separator = seperator; |
---|
| 343 | return this; |
---|
| 344 | } |
---|
| 345 | |
---|
| 346 | /// <summary> |
---|
| 347 | /// Creates and initializes the report. After calling Build(), this builder instance |
---|
| 348 | /// is reset and can be reused to create a new report. |
---|
| 349 | /// </summary> |
---|
| 350 | /// <exception cref="InvalidOperationException">Thrown when no indicators have been added.</exception> |
---|
| 351 | /// <returns>The created report instance.</returns> |
---|
| 352 | public Report Build() { |
---|
| 353 | if (!instance.keys.Any()) |
---|
| 354 | throw new InvalidOperationException("Nothing to build: No indicators have been added to the Builder."); |
---|
| 355 | var result = instance; |
---|
| 356 | instance = new Report(); |
---|
| 357 | result.Initialize(); |
---|
| 358 | return result; |
---|
| 359 | } |
---|
| 360 | |
---|
| 361 | private static readonly int[] numToBits = new int[16] { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; |
---|
| 362 | private static int CountSetBits(int num) { |
---|
| 363 | return numToBits[num & 0xf] + |
---|
| 364 | numToBits[(num >> 4) & 0xf] + |
---|
| 365 | numToBits[(num >> 8) & 0xf] + |
---|
| 366 | numToBits[(num >> 16) & 0xf] + |
---|
| 367 | numToBits[(num >> 20) & 0xf] + |
---|
| 368 | numToBits[(num >> 24) & 0xf] + |
---|
| 369 | numToBits[(num >> 28) & 0xf]; |
---|
| 370 | } |
---|
| 371 | } |
---|
| 372 | |
---|
| 373 | private class Key { |
---|
| 374 | public string Name { get; set; } |
---|
| 375 | public INumericMonitor Statistics { get; set; } |
---|
| 376 | public Measures Measure { get; set; } |
---|
| 377 | public int TotalMeasures { get; set; } |
---|
| 378 | } |
---|
| 379 | } |
---|
| 380 | |
---|
| 381 | } |
---|