Skip to content

Commit 94f379e

Browse files
committed
Finish implementation of pseudo realtime simulation and increase target
to .NET Core 2.1 * Fix interrupting the realtime delay * Add unit tests * Introduce a PseudoRealtimeProcess that will always reset the environment to realtime
1 parent ea2d8fb commit 94f379e

File tree

6 files changed

+279
-92
lines changed

6 files changed

+279
-92
lines changed

src/Benchmark/Benchmark.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>netcoreapp2.0</TargetFramework>
5+
<TargetFramework>netcoreapp2.1</TargetFramework>
66
<Authors>Andreas Beham</Authors>
77
<Version>3.2</Version>
88
<Company>HEAL, FH Upper Austria</Company>

src/Samples/Samples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>netcoreapp2.0</TargetFramework>
5+
<TargetFramework>netcoreapp2.1</TargetFramework>
66
<Version>3.2</Version>
77
<Authors>Andreas Beham</Authors>
88
<Company>HEAL, FH Upper Austria</Company>

src/SimSharp/Core/Environment.cs

Lines changed: 143 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using System.Collections.Generic;
10+
using System.Diagnostics;
1011
using System.IO;
1112
using System.Threading;
1213
using System.Threading.Tasks;
@@ -17,7 +18,7 @@ namespace SimSharp {
1718
/// </summary>
1819
/// <remarks>
1920
/// This class is not thread-safe against manipulation of the event queue. If you supply a termination
20-
/// event that is set outside the simulation, please use the <see cref="ThreadSafeSimulation"/> environment.
21+
/// event that is set outside the simulation thread, please use the <see cref="ThreadSafeSimulation"/> environment.
2122
///
2223
/// For most purposes <see cref="Simulation"/> is however the better and faster choice.
2324
/// </remarks>
@@ -37,10 +38,11 @@ public double NowD {
3738
get { return (Now - StartDate).TotalSeconds / DefaultTimeStepSeconds; }
3839
}
3940

41+
private DateTime now;
4042
/// <summary>
4143
/// The current simulation time as a calendar date.
4244
/// </summary>
43-
public DateTime Now { get; protected set; }
45+
public virtual DateTime Now { get => now; protected set => now = value; }
4446

4547
/// <summary>
4648
/// The calendar date when the simulation started. This defaults to 1970-1-1 if
@@ -206,7 +208,6 @@ public virtual object Run(Event stopEvent = null) {
206208
var stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
207209
while (!stop) {
208210
Step();
209-
ProcessedEvents++;
210211
stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
211212
}
212213
} catch (StopSimulationException e) { OnRunFinished(); return e.Value; }
@@ -242,6 +243,7 @@ public virtual void Step() {
242243
Now = next.PrimaryPriority;
243244
evt = next.Event;
244245
evt.Process();
246+
ProcessedEvents++;
245247
}
246248

247249
/// <summary>
@@ -373,6 +375,7 @@ public TimeSpan RandExponential(TimeSpan mean) {
373375

374376
private bool useSpareNormal = false;
375377
private double spareNormal = double.NaN;
378+
376379
/// <summary>
377380
/// Uses the Marsaglia polar method to generate a random variable
378381
/// from two uniform random distributed values.
@@ -803,7 +806,6 @@ public override object Run(Event stopEvent = null) {
803806
}
804807
while (!stop) {
805808
Step();
806-
ProcessedEvents++;
807809
lock (_locker) {
808810
stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
809811
}
@@ -815,6 +817,23 @@ public override object Run(Event stopEvent = null) {
815817
return stopEvent.Value;
816818
}
817819

820+
public Task<object> RunAsync(TimeSpan duration) {
821+
return Task.Run(() => Run(duration));
822+
}
823+
824+
public Task<object> RunAsync(DateTime until) {
825+
return Task.Run(() => Run(until));
826+
}
827+
828+
/// <summary>
829+
/// Run until a certain event is processed, but does not block.
830+
/// </summary>
831+
/// <param name="stopEvent">The event that stops the simulation.</param>
832+
/// <returns></returns>
833+
public Task<object> RunAsync(Event stopEvent = null) {
834+
return Task.Run(() => Run(stopEvent));
835+
}
836+
818837
/// <summary>
819838
/// Performs a single step of the simulation, i.e. process a single event
820839
/// </summary>
@@ -829,6 +848,7 @@ public override void Step() {
829848
evt = next.Event;
830849
}
831850
evt.Process();
851+
ProcessedEvents++;
832852
}
833853

834854
/// <summary>
@@ -857,60 +877,115 @@ public override DateTime Peek() {
857877
}
858878
}
859879

860-
public class PseudoRealTimeSimulation : ThreadSafeSimulation {
861-
private const double DefaultRealTimeFactor = 1.0;
862-
863-
public double? RealTimeFactor { get; protected set; } = DefaultRealTimeFactor;
864-
public bool IsRunningInRealTime => RealTimeFactor.HasValue;
880+
/// <summary>
881+
/// Provides a simulation environment where delays in simulation time may result in a similar
882+
/// delay in wall-clock time. The environment is not an actual realtime simulation environment
883+
/// in that there is no guarantee that 3 seconds in model time are also exactly 3 seconds in
884+
/// observed wall-clock time. This simulation environment is a bit slower, as the overhead of
885+
/// the simulation kernel (event creation, queuing, processing, etc.) is not accounted for.
886+
///
887+
/// However, it features a switch between virtual and realtime, thus allowing it to be used
888+
/// in contexts where realtime is only necessary sometimes (e.g. during interaction with
889+
/// long-running co-processes). Such use cases may arise in simulation control problems.
890+
/// </summary>
891+
public class PseudoRealtimeSimulation : ThreadSafeSimulation {
892+
public const double DefaultRealtimeScale = 1;
865893

866-
public PseudoRealTimeSimulation() : this(new DateTime(1970, 1, 1)) { }
867-
public PseudoRealTimeSimulation(TimeSpan? defaultStep) : this(new DateTime(1970, 1, 1), defaultStep) { }
868-
public PseudoRealTimeSimulation(DateTime initialDateTime, TimeSpan? defaultStep = null) : this(new PcgRandom(), initialDateTime, defaultStep) { }
869-
public PseudoRealTimeSimulation(int randomSeed, TimeSpan? defaultStep = null) : this(new DateTime(1970, 1, 1), randomSeed, defaultStep) { }
870-
public PseudoRealTimeSimulation(DateTime initialDateTime, int randomSeed, TimeSpan? defaultStep = null) : this(new PcgRandom(randomSeed), initialDateTime, defaultStep) { }
871-
public PseudoRealTimeSimulation(IRandom random, DateTime initialDateTime, TimeSpan? defaultStep = null) : base(random, initialDateTime, defaultStep) { }
894+
/// <summary>
895+
/// The scale at which the simulation runs in comparison to realtime. A value smaller
896+
/// than 1 results in longer-than-realtime delays, while a value larger than 1 results
897+
/// in shorter-than-realtime delays. A value of exactly 1 is realtime.
898+
/// </summary>
899+
public double? RealtimeScale { get; protected set; } = DefaultRealtimeScale;
900+
/// <summary>
901+
/// Whether a non-null <see cref="RealtimeScale"/> has been set.
902+
/// </summary>
903+
public bool IsRunningInRealtime => RealtimeScale.HasValue;
872904

873-
public override void StopAsync() {
874-
base.StopAsync();
905+
private object _timeLocker = new object();
906+
/// <summary>
907+
/// The current model time. Note that, while in realtime, this may continuously change.
908+
/// </summary>
909+
public override DateTime Now {
910+
get { lock (_timeLocker) { return base.Now + _rtDelayTime.Elapsed; } }
911+
protected set => base.Now = value;
875912
}
876913

877-
protected CancellationTokenSource _delay = null;
914+
protected CancellationTokenSource _rtDelayCtrl = null;
915+
protected Stopwatch _rtDelayTime = new Stopwatch();
916+
917+
918+
public PseudoRealtimeSimulation() : this(new DateTime(1970, 1, 1)) { }
919+
public PseudoRealtimeSimulation(TimeSpan? defaultStep) : this(new DateTime(1970, 1, 1), defaultStep) { }
920+
public PseudoRealtimeSimulation(DateTime initialDateTime, TimeSpan? defaultStep = null) : this(new PcgRandom(), initialDateTime, defaultStep) { }
921+
public PseudoRealtimeSimulation(int randomSeed, TimeSpan? defaultStep = null) : this(new DateTime(1970, 1, 1), randomSeed, defaultStep) { }
922+
public PseudoRealtimeSimulation(DateTime initialDateTime, int randomSeed, TimeSpan? defaultStep = null) : this(new PcgRandom(randomSeed), initialDateTime, defaultStep) { }
923+
public PseudoRealtimeSimulation(IRandom random, DateTime initialDateTime, TimeSpan? defaultStep = null) : base(random, initialDateTime, defaultStep) { }
924+
925+
protected override EventQueueNode DoSchedule(DateTime date, Event @event, int priority = 0) {
926+
if (ScheduleQ.Count > 0 && date < ScheduleQ.First.PrimaryPriority) _rtDelayCtrl?.Cancel();
927+
return base.DoSchedule(date, @event, priority);
928+
}
878929

879930
public override void Step() {
880-
if (IsRunningInRealTime) {
931+
var delay = TimeSpan.Zero;
932+
double? rtScale = null;
933+
lock (_locker) {
934+
if (IsRunningInRealtime) {
935+
rtScale = RealtimeScale;
936+
var next = ScheduleQ.First.PrimaryPriority;
937+
delay = next - base.Now;
938+
if (rtScale.Value != 1.0) delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds / rtScale.Value);
939+
_rtDelayCtrl = CancellationTokenSource.CreateLinkedTokenSource(_stop.Token);
940+
}
941+
}
942+
943+
if (delay > TimeSpan.Zero) {
944+
_rtDelayTime.Start();
945+
Task.Delay(delay, _rtDelayCtrl.Token).ContinueWith(_ => { }).Wait();
946+
_rtDelayTime.Stop();
947+
var observed = _rtDelayTime.Elapsed;
948+
881949
lock (_locker) {
882-
if (IsRunningInRealTime) {
883-
var next = ScheduleQ.First.PrimaryPriority;
884-
var delay = next - Now;
885-
if (RealTimeFactor.Value != 1.0) delay = TimeSpan.FromMilliseconds(delay.Milliseconds / RealTimeFactor.Value);
886-
if (delay > TimeSpan.Zero) {
887-
_delay = CancellationTokenSource.CreateLinkedTokenSource(_stop.Token);
888-
var then = DateTime.UtcNow;
889-
Task.Delay(delay, _delay.Token).Wait();
890-
if (_delay.IsCancellationRequested) {
891-
var now = DateTime.UtcNow;
892-
var observedDelay = now - then;
893-
if (observedDelay < delay) {
894-
Now += observedDelay;
895-
return; // next event is not processed
896-
}
897-
}
950+
if (rtScale.Value != 1.0) observed = TimeSpan.FromMilliseconds(observed.TotalMilliseconds / rtScale.Value);
951+
if (_rtDelayCtrl.IsCancellationRequested && observed < delay) {
952+
lock (_timeLocker) {
953+
Now = base.Now + observed;
954+
_rtDelayTime.Reset();
898955
}
956+
return; // next event is not processed, step is not actually completed
899957
}
900958
}
901959
}
902-
base.Step();
960+
961+
Event evt;
962+
lock (_locker) {
963+
var next = ScheduleQ.Dequeue();
964+
lock (_timeLocker) {
965+
_rtDelayTime.Reset();
966+
Now = next.PrimaryPriority;
967+
}
968+
evt = next.Event;
969+
}
970+
evt.Process();
971+
ProcessedEvents++;
903972
}
904973

905974
/// <summary>
906-
/// Switches the simulation to virtual time mode. In this mode, events
907-
/// are processed without delay just like in a <see cref="ThreadSafeSimulation"/>.
975+
/// Switches the simulation to virtual time mode, i.e., running as fast as possible.
976+
/// In this mode, events are processed without delay just like in a <see cref="ThreadSafeSimulation"/>.
908977
/// </summary>
909-
public virtual void SwitchToVirtualTime() {
910-
if (!IsRunningInRealTime) return;
978+
/// <remarks>
979+
/// An ongoing real-time delay is being canceled when this method is called. Usually, this
980+
/// is only the case when this method is called from a thread other than the main simulation thread.
981+
///
982+
/// If the simulation is already in virtual time mode, this method has no effect.
983+
/// </remarks>
984+
public virtual void SetVirtualtime() {
911985
lock (_locker) {
912-
RealTimeFactor = null;
913-
_delay?.Cancel(); // TODO: Same lock region
986+
if (!IsRunningInRealtime) return;
987+
RealtimeScale = null;
988+
_rtDelayCtrl?.Cancel();
914989
}
915990
}
916991

@@ -919,21 +994,37 @@ public virtual void SwitchToVirtualTime() {
919994
/// this default mode is configurable.
920995
/// </summary>
921996
/// <remarks>
922-
/// Per default, a <see cref="PseudoRealTimeSimulation"/> is executed
923-
/// in real time with a simulation speed factor of 1.0.
997+
/// If this method is called while running in real-time mode, but given a different
998+
/// <paramref name="realtimeScale"/>, the current delay is canceled and the remaining
999+
/// time is delayed using the new time factor.
9241000
///
925-
/// With a factor of 1.0, a timeout of 5.0 seconds would delay the
926-
/// simulation for 5.0 seconds. With a factor of 2.0, the same timeout
927-
/// would delay the simulation for 2.5 seconds, whereas a factor of
928-
/// 0.5 would delay the simulation for 10.0 seconds.
1001+
/// The default factor is 1, i.e., real time - a timeout of 5 seconds would cause
1002+
/// a wall-clock delay of 5 seconds. With a factor of 2, the delay as measured by
1003+
/// a wall clock would be 2.5 seconds, whereas a factor of 0.5, a wall-clock delay of
1004+
/// 10 seconds would be observed.
9291005
/// </remarks>
930-
/// <param name="realTimeFactor">A factor greater than 0.0 used to scale real time events (higher value = faster execution).</param>
931-
public virtual void SwitchToRealTime(double realTimeFactor = DefaultRealTimeFactor) {
1006+
/// <param name="realtimeScale">A value strictly greater than 0 used to scale real time events.</param>
1007+
public virtual void SetRealtime(double realtimeScale = DefaultRealtimeScale) {
9321008
lock (_locker) {
933-
if (realTimeFactor <= 0.0) throw new ArgumentException("The simulation speed scaling factor must be strictly positive.", nameof(realTimeFactor));
934-
RealTimeFactor = realTimeFactor;
1009+
if (realtimeScale <= 0.0) throw new ArgumentException("The simulation speed scaling factor must be strictly positive.", nameof(realtimeScale));
1010+
if (IsRunningInRealtime && realtimeScale != RealtimeScale) _rtDelayCtrl?.Cancel();
1011+
RealtimeScale = realtimeScale;
9351012
}
9361013
}
1014+
1015+
/// <summary>
1016+
/// This is only a convenience for mixed real- and virtual time simulations.
1017+
/// It creates a new pseudo realtime process which will set the simulation
1018+
/// to realtime every time it continues (e.g., if it has been set to virtual time).
1019+
/// The process is automatically scheduled to be started at the current simulation time.
1020+
/// </summary>
1021+
/// <param name="generator">The generator function that represents the process.</param>
1022+
/// <param name="priority">The priority to rank events at the same time (smaller value = higher priority).</param>
1023+
/// <param name="realtimeScale">A value strictly greater than 0 used to scale real time events (1 = realtime).</param>
1024+
/// <returns>The scheduled process that was created.</returns>
1025+
public Process PseudoRealtimeProcess(IEnumerable<Event> generator, int priority = 0, double realtimeScale = DefaultRealtimeScale) {
1026+
return new PseudoRealtimeProcess(this, generator, priority, realtimeScale);
1027+
}
9371028
}
9381029

9391030
/// <summary>

src/SimSharp/Core/Events/Process.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,31 @@ public Initialize(Simulation environment, Process process, int priority)
153153
}
154154
}
155155
}
156+
157+
public class PseudoRealtimeProcess : Process {
158+
public double RealtimeScale { get; set; }
159+
160+
public new PseudoRealtimeSimulation Environment {
161+
get { return (PseudoRealtimeSimulation)base.Environment; }
162+
}
163+
164+
/// <summary>
165+
/// Sets up a new process.
166+
/// The process places an initialize event into the event queue which starts
167+
/// the process by retrieving events from the generator.
168+
/// </summary>
169+
/// <param name="environment">The environment in which the process lives.</param>
170+
/// <param name="generator">The generator function of the process.</param>
171+
/// <param name="priority">The priority if multiple processes are started at the same time.</param>
172+
/// <param name="realtimeScale">A value strictly greater than 0 used to scale real time events (1 = realtime).</param>
173+
public PseudoRealtimeProcess(PseudoRealtimeSimulation environment, IEnumerable<Event> generator, int priority = 0, double realtimeScale = PseudoRealtimeSimulation.DefaultRealtimeScale)
174+
: base(environment, generator, priority) {
175+
RealtimeScale = realtimeScale;
176+
}
177+
178+
protected override void Resume(Event @event) {
179+
Environment.SetRealtime(RealtimeScale);
180+
base.Resume(@event);
181+
}
182+
}
156183
}

0 commit comments

Comments
 (0)