Skip to content

Commit 284e74c

Browse files
committed
Add pseudo real time simulation environment
This commit adds a pseudo real time environment with the ability to switch between real time and virtual time. Furthermore, it adds a test that shows the general idea. When switching from virtual time to real time, the remaining time of following events is delayed. A factor can be used to scale real time execution times.
1 parent b45a01e commit 284e74c

File tree

2 files changed

+155
-10
lines changed

2 files changed

+155
-10
lines changed

src/SimSharp/Core/Environment.cs

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.IO;
11+
using System.Threading;
12+
using System.Threading.Tasks;
1113

1214
namespace SimSharp {
1315
/// <summary>
@@ -177,7 +179,7 @@ public virtual object Run(DateTime until) {
177179
return Run(stopEvent);
178180
}
179181

180-
protected bool _stopRequested = false;
182+
protected CancellationTokenSource _stop = null;
181183
/// <summary>
182184
/// Run until a certain event is processed.
183185
/// </summary>
@@ -192,7 +194,7 @@ public virtual object Run(DateTime until) {
192194
/// <param name="stopEvent">The event that stops the simulation.</param>
193195
/// <returns></returns>
194196
public virtual object Run(Event stopEvent = null) {
195-
_stopRequested = false;
197+
_stop = new CancellationTokenSource();
196198
if (stopEvent != null) {
197199
if (stopEvent.IsProcessed) {
198200
return stopEvent.Value;
@@ -201,21 +203,21 @@ public virtual object Run(Event stopEvent = null) {
201203
}
202204
OnRunStarted();
203205
try {
204-
var stop = ScheduleQ.Count == 0 || _stopRequested;
206+
var stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
205207
while (!stop) {
206208
Step();
207209
ProcessedEvents++;
208-
stop = ScheduleQ.Count == 0 || _stopRequested;
210+
stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
209211
}
210212
} catch (StopSimulationException e) { OnRunFinished(); return e.Value; }
211213
OnRunFinished();
212214
if (stopEvent == null) return null;
213-
if (!_stopRequested && !stopEvent.IsTriggered) throw new InvalidOperationException("No scheduled events left but \"until\" event was not triggered.");
215+
if (!_stop.IsCancellationRequested && !stopEvent.IsTriggered) throw new InvalidOperationException("No scheduled events left but \"until\" event was not triggered.");
214216
return stopEvent.Value;
215217
}
216218

217219
public virtual void StopAsync() {
218-
_stopRequested = true;
220+
_stop?.Cancel();
219221
}
220222

221223
public event EventHandler RunStarted;
@@ -786,7 +788,7 @@ public override void Schedule(TimeSpan delay, Event @event, int priority = 0) {
786788
/// <param name="stopEvent">The event that stops the simulation.</param>
787789
/// <returns></returns>
788790
public override object Run(Event stopEvent = null) {
789-
_stopRequested = false;
791+
_stop = new CancellationTokenSource();
790792
if (stopEvent != null) {
791793
if (stopEvent.IsProcessed) {
792794
return stopEvent.Value;
@@ -797,19 +799,19 @@ public override object Run(Event stopEvent = null) {
797799
try {
798800
var stop = false;
799801
lock (_locker) {
800-
stop = ScheduleQ.Count == 0 || _stopRequested;
802+
stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
801803
}
802804
while (!stop) {
803805
Step();
804806
ProcessedEvents++;
805807
lock (_locker) {
806-
stop = ScheduleQ.Count == 0 || _stopRequested;
808+
stop = ScheduleQ.Count == 0 || _stop.IsCancellationRequested;
807809
}
808810
}
809811
} catch (StopSimulationException e) { OnRunFinished(); return e.Value; }
810812
OnRunFinished();
811813
if (stopEvent == null) return null;
812-
if (!_stopRequested && !stopEvent.IsTriggered) throw new InvalidOperationException("No scheduled events left but \"until\" event was not triggered.");
814+
if (!_stop.IsCancellationRequested && !stopEvent.IsTriggered) throw new InvalidOperationException("No scheduled events left but \"until\" event was not triggered.");
813815
return stopEvent.Value;
814816
}
815817

@@ -855,6 +857,72 @@ public override DateTime Peek() {
855857
}
856858
}
857859

860+
public class PseudoRealTimeSimulation : ThreadSafeSimulation {
861+
private const double DefaultRealTimeFactor = 1.0;
862+
private const double RealTimeThreshold = 1000.0;
863+
864+
public double RealTimeFactor { get; private set; } = DefaultRealTimeFactor;
865+
public bool IsRunningInRealTime => RealTimeFactor < RealTimeThreshold;
866+
867+
public PseudoRealTimeSimulation() : this(new DateTime(1970, 1, 1)) { }
868+
public PseudoRealTimeSimulation(TimeSpan? defaultStep) : this(new DateTime(1970, 1, 1), defaultStep) { }
869+
public PseudoRealTimeSimulation(DateTime initialDateTime, TimeSpan? defaultStep = null) : this(new PcgRandom(), initialDateTime, defaultStep) { }
870+
public PseudoRealTimeSimulation(int randomSeed, TimeSpan? defaultStep = null) : this(new DateTime(1970, 1, 1), randomSeed, defaultStep) { }
871+
public PseudoRealTimeSimulation(DateTime initialDateTime, int randomSeed, TimeSpan? defaultStep = null) : this(new PcgRandom(randomSeed), initialDateTime, defaultStep) { }
872+
public PseudoRealTimeSimulation(IRandom random, DateTime initialDateTime, TimeSpan? defaultStep = null) : base(random, initialDateTime, defaultStep) { }
873+
874+
public override void StopAsync() {
875+
base.StopAsync();
876+
}
877+
878+
public override void Step() {
879+
lock (_locker) {
880+
if (IsRunningInRealTime) {
881+
var next = ScheduleQ.First.PrimaryPriority;
882+
var delay = next - Now;
883+
if (RealTimeFactor != 1.0) delay = TimeSpan.FromMilliseconds(delay.Milliseconds * 1.0 / RealTimeFactor);
884+
if (delay > TimeSpan.Zero) Task.Delay(delay, _stop.Token).Wait();
885+
}
886+
}
887+
base.Step();
888+
}
889+
890+
/// <summary>
891+
/// Switches the simulation to virtual time mode. In this mode, events
892+
/// are processed without delay just like in a ThreadSafeSimulation.
893+
/// </summary>
894+
public virtual void SwitchToVirtualTime() {
895+
lock (_locker) {
896+
RealTimeFactor = RealTimeThreshold;
897+
}
898+
}
899+
900+
/// <summary>
901+
/// Switches the simulation to real time mode. The real time factor of
902+
/// this default mode is configurable.
903+
/// </summary>
904+
/// <remarks>
905+
/// Per default, a <see cref="PseudoRealTimeSimulation"/> is executed
906+
/// in real time with a simulation speed factor of 1.0.
907+
///
908+
/// With a factor of 1.0, a timeout of 5.0 seconds would delay the
909+
/// simulation for 5.0 seconds. With a factor of 2.0, the same timeout
910+
/// would delay the simulation for 2.5 seconds, whereas a factor of
911+
/// 0.5 would delay the simulation for 10.0 seconds.
912+
/// </remarks>
913+
/// <param name="realTimeFactor">A factor greater than 0.0 used to scale real time events (higher value = faster execution).</param>
914+
public virtual void SwitchToRealTime(double realTimeFactor = DefaultRealTimeFactor) {
915+
lock (_locker) {
916+
if (RealTimeFactor <= 0.0) throw new ArgumentException("The simulation speed scaling factor must not be negative.", nameof(realTimeFactor));
917+
if (realTimeFactor >= RealTimeThreshold) {
918+
SwitchToVirtualTime();
919+
} else {
920+
RealTimeFactor = realTimeFactor;
921+
}
922+
}
923+
}
924+
}
925+
858926
/// <summary>
859927
/// Environments hold the event queues, schedule and process events.
860928
/// </summary>

src/Tests/EnvironmentTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.Linq;
11+
using System.Threading.Tasks;
1112
using Xunit;
1213

1314
namespace SimSharp.Tests {
@@ -164,5 +165,81 @@ public void EnvironmentBackwardsCompat() {
164165
0.65864875161591829, 0.46713487767696055, -0.37878389025311837};
165166
Assert.Equal(old, rndNumbers);
166167
}
168+
169+
[Fact]
170+
public void PseudoRealTimeEnvTestStopTest() {
171+
var then = DateTime.UtcNow;
172+
var env = new PseudoRealTimeSimulation();
173+
env.Run(TimeSpan.FromSeconds(1));
174+
var now = DateTime.UtcNow;
175+
Assert.True(now - then >= TimeSpan.FromSeconds(1));
176+
177+
var t = Task.Run(() => env.Run(TimeSpan.FromMinutes(1)));
178+
Task.Delay(TimeSpan.FromMilliseconds(200)).Wait();
179+
env.StopAsync();
180+
Task.Delay(TimeSpan.FromMilliseconds(200)).Wait();
181+
Assert.True(t.IsCompleted);
182+
}
183+
184+
[Fact]
185+
public void PseudoRealTimeEnvTest() {
186+
var then = DateTime.UtcNow;
187+
var delay = TimeSpan.FromSeconds(1);
188+
var env = new PseudoRealTimeSimulation();
189+
env.Process(RealTimeDelay(env, delay));
190+
env.Run();
191+
var now = DateTime.UtcNow;
192+
Assert.True(now - then >= delay);
193+
}
194+
195+
private IEnumerable<Event> RealTimeDelay(Simulation env, TimeSpan delay) {
196+
yield return env.Timeout(delay);
197+
}
198+
199+
[Fact]
200+
public void MixedPseudoRealTimeEnvTest() {
201+
var delay0 = TimeSpan.FromSeconds(8.0);
202+
var delay1 = TimeSpan.FromSeconds(1.0);
203+
var delay2 = TimeSpan.FromSeconds(5.0);
204+
var env = new PseudoRealTimeSimulation();
205+
206+
// process 1
207+
env.Process(MixedTestRealTimeDelay(env, delay0, delay1, delay2)); // should take at least 8 - 6 + 1 = 3 seconds in real time
208+
// process 2
209+
env.Process(MixedTestVirtualTimeDelay(env, delay0, delay1, delay2)); // should take 1 second in real and 5 seconds in virtual time
210+
211+
var then = DateTime.UtcNow;
212+
env.Run();
213+
var now = DateTime.UtcNow;
214+
Assert.True(now - then >= delay0 - delay2); // delay2 is virtual
215+
}
216+
217+
// yields events for process 1
218+
private IEnumerable<Event> MixedTestRealTimeDelay(PseudoRealTimeSimulation env, TimeSpan delay0, TimeSpan delay1, TimeSpan delay2) {
219+
Assert.True(env.Now == env.StartDate);
220+
var then = DateTime.UtcNow;
221+
yield return env.Timeout(delay0);
222+
var now = DateTime.UtcNow;
223+
Assert.True(env.Now == env.StartDate + delay0);
224+
Assert.True(now - then >= delay0 - delay2); // delay2 is virtual
225+
}
226+
227+
// yields events for process 2
228+
private IEnumerable<Event> MixedTestVirtualTimeDelay(PseudoRealTimeSimulation env, TimeSpan delay0, TimeSpan delay1, TimeSpan delay2) {
229+
Assert.True(env.Now == env.StartDate);
230+
var then = DateTime.UtcNow;
231+
yield return env.Timeout(delay1); // delays 1 second in real time
232+
var now = DateTime.UtcNow;
233+
Assert.True(env.Now == env.StartDate + delay1);
234+
Assert.True(now - then >= delay1);
235+
236+
env.SwitchToVirtualTime();
237+
238+
yield return env.Timeout(delay2); // delays 5 seconds in virtual time
239+
Assert.True(env.Now == env.StartDate + delay1 + delay2);
240+
241+
// switch back to real time happens at virtual time 00:00:06
242+
env.SwitchToRealTime(); // real time timeout in process 1 should delay at least 8 - 6 + 1 = 3 seconds
243+
}
167244
}
168245
}

0 commit comments

Comments
 (0)