Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/HeuristicLab.ExtLibs/HeuristicLab.SimSharp/3.3.1/SimSharp-3.3.1/Analysis/Report.cs @ 17487

Last change on this file since 17487 was 17487, checked in by abeham, 4 years ago

#3065: update Sim# to 3.3.1

File size: 15.9 KB
Line 
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
8using System;
9using System.Collections.Generic;
10using System.IO;
11using System.Linq;
12
13namespace 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}
Note: See TracBrowser for help on using the repository browser.