Skip to content

Commit

Permalink
Add distribution classes (#39)
Browse files Browse the repository at this point in the history
* Add Distribution classes and refactor Rand* methods
* Add static class for easier creation of distributions
 - Adapt samples
 - Adapt documentation
  • Loading branch information
abeham authored Jan 12, 2022
1 parent 06e46f9 commit a02e6d2
Show file tree
Hide file tree
Showing 24 changed files with 1,440 additions and 222 deletions.
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ Sim# aims to port the concepts used in SimPy [1] to the .NET world. Sim# is impl

Sim# allows modeling processes easily and with little boiler plate code. A process is described as a method that yields events. When an event is yielded, the process waits on it. Processes are themselves events and so it is convenient to spawn sub-processes that can either be waited upon or that run next to each other. There is no need to inherit from classes or understand a complex object oriented design.

To demonstrate how simple models can be expressed with little code, consider a model of an m/m/1 queuing system as expressed in Sim#:
To demonstrate how simple models can be expressed with little code, consider a model of an m/m/1 queuing system as expressed in the current version of Sim#:

```csharp
TimeSpan ARRIVAL_TIME = TimeSpan.FromSeconds(...);
TimeSpan PROCESSING_TIME = TimeSpan.FromSeconds(...);
using static SimSharp.Distributions;

ExponentialTime ARRIVAL = EXP(TimeSpan.FromSeconds(...));
ExponentialTime PROCESSING = EXP(TimeSpan.FromSeconds(...));
TimeSpan SIMULATION_TIME = TimeSpan.FromHours(...);

IEnumerable<Event> MM1Q(Simulation env, Resource server) {
while (true) {
yield return env.TimeoutExponential(ARRIVAL_TIME);
yield return env.Timeout(ARRIVAL);
env.Process(Item(env, server));
}
}

IEnumerable<Event> Item(Simulation env, Resource server) {
using (var s = server.Request()) {
yield return s;
yield return env.TimeoutExponential(PROCESSING_TIME);
yield return env.Timeout(PROCESSING);
Console.WriteLine("Duration {0}", env.Now - s.Time);
}
}

void RunSimulation() {
var env = new Simulation(randomSeed: 42);
var server = new Resource(env, capacity: 1) {
QueueLength = new TimeSeriesMonitor(env, collect: true)
QueueLength = new TimeSeriesMonitor(env, collect: true)
};
env.Process(MM1Q(env, server));
env.Run(SIMULATION_TIME);
Expand All @@ -65,7 +67,15 @@ Also in Sim# it was decided to base the unit for current time and delays on `Dat
var env = new Simulation(defaultStep: TimeSpan.FromMinutes(1));
```

In that environment, calling `env.TimeoutD(1)` would be equal to calling the more elaborate standard API `env.Timeout(TimeSpan.FromMinutes(1))`.
In that environment, calling `env.TimeoutD(1)` would be equal to calling the more elaborate standard API `env.Timeout(TimeSpan.FromMinutes(1))`. In case timeouts are sampled from a distribution, it is important to distinguish the `TimeoutD(IDistribution<double>)`and `Timeout(IDistribution<TimeSpan>)` methods. Again, the former assumes the unit that is given in `defaultStep`, e.g., minutes as in the case above. For instance, `env.TimeoutD(new Exponential(2))` would indicate a mean of 2 minutes in the above environment, while `env.Timeout(new Exponential(TimeSpan.FromMinutes(2))` would always mean two minutes, regardless of the `defaultStep`. In generally, the `TimeSpan` API is preferred as it already expresses time in the appropriate units.

For shortcuts of the distribution classes a static class `Distributions` exists. You can put `using static SimSharp.Distributions;` in the using declarations and then use those methods without a qualifier. The following code snippet shows this feature.

```csharp
using static SimSharp.Distributions;
// ... additional code excluded
yield return env.TimeoutD(UNIF(10, 20));
```

## References

Expand Down
26 changes: 26 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The documentation covers the following aspects:
+ [Resources](#resources)
+ [Samples](#samples)
+ [Environments](#environments)
+ [Distributions](#distributions)
* [Monitoring](#monitoring)
+ [Reports](#reports)

Expand Down Expand Up @@ -100,6 +101,31 @@ These methods are thread safe and may be called from different threads.

Obtaining the current simulation time using `Now` in realtime mode will account for the duration that the simulation is sleeping. Thus, calling this property from a different thread will always receive the actual simulation time, i.e., the time of the last event plus the elapsed time.

### Distributions
Starting from Sim# 3.4, distributions are included as classes. This allows parameterizing processes with the actual distributions instead of having to predetermine the distribution within the process. For instance, a process that describes a customer arrival can now be specified and instantiated with different distribution types, e.g., exponential, triangular, and others. Creating a distribution is easy and cheap. For best readability, it is advised to include `SimSharp.Distributions` in the using section as a `using static`.

```csharp
using SimSharp;
using static SimSharp.Distributions;

namespace MyApplication {
public class MyModel {
IEnumerable<Event> CustomerArrival(Simulation env, IDistribution<TimeSpan> arrivalDist) {
while (true) {
yield return env.Timeout(arrivalDist);
// Logic of creating a customer
}
}

public void Run() {
var env = new Simulation();
// Exponential distribution with a mean of 5
env.Process(CustomerArrival(env, EXP(TimeSpan.FromMinutes(5))));
env.Run(TimeSpan.FromHours(24));
}
}
}

### Samples

Processes that interact with common resources may create highly dynamic behavior which may not be analytically tractable and thus have to be simulated. Sim# includes a number of samples that, while being easy to understand and simple, show how to model certain processes such as preemption, interruption, handover of resource requests and more. A short summary of the provided samples together with the highlights are given in the following:
Expand Down
10 changes: 5 additions & 5 deletions src/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"name": "Benchmark MachineShop",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"preLaunchTask": "releasebuild",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Benchmark/bin/Debug/netcoreapp2.0/Benchmark.dll",
"program": "${workspaceFolder}/Benchmark/bin/Release/netcoreapp2.1/Benchmark.dll",
"args": ["machineshop"],
"cwd": "${workspaceFolder}/Benchmark",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
Expand All @@ -22,9 +22,9 @@
"name": "Benchmark Synthetic",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"preLaunchTask": "releasebuild",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Benchmark/bin/Debug/netcoreapp2.0/Benchmark.dll",
"program": "${workspaceFolder}/Benchmark/bin/Release/netcoreapp2.1/Benchmark.dll",
"args": ["synthetic", "--repetitions", "3", "--time", "60", "--cpufreq", "2.9"],
"cwd": "${workspaceFolder}/Benchmark",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
Expand All @@ -38,7 +38,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Samples/bin/Debug/netcoreapp2.0/Samples.dll",
"program": "${workspaceFolder}/Samples/bin/Debug/netcoreapp2.1/Samples.dll",
"args": [],
"cwd": "${workspaceFolder}/Samples",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
Expand Down
18 changes: 18 additions & 0 deletions src/.vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@
"panel": "dedicated"
}
},
{
"label": "releasebuild",
"command": "dotnet",
"type": "process",
"args": [
"build",
"-c",
"Release",
"${workspaceFolder}/SimSharp.sln"
],
"problemMatcher": "$msCompile",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated"
}
},
{
"label": "test",
"command": "dotnet",
Expand Down
6 changes: 3 additions & 3 deletions src/Benchmark/Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Authors>Andreas Beham</Authors>
<Version>3.3</Version>
<Version>3.4</Version>
<Company>HEAL, FH Upper Austria</Company>
<Product>Sim# (Benchmarks)</Product>
<Description />
<Copyright>Andreas Beham</Copyright>
<PackageLicenseUrl>https://raw.githubusercontent.com/abeham/SimSharp/master/LICENSE.txt</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/abeham/SimSharp</PackageProjectUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/heal-research/SimSharp/master/LICENSE.txt</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/heal-research/SimSharp</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/Benchmark/MachineShopBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static SimSharp.Distributions;

namespace SimSharp.Benchmarks {
public class MachineShopBenchmark {
Expand Down Expand Up @@ -36,9 +37,8 @@ public static int Run(MachineShopOptions opts) {
* with the machine repair. The workshop works continuously.
*/
private const int RandomSeed = 42;
private const double PtMean = 10.0; // Avg. processing time in minutes
private const double PtSigma = 2.0; // Sigma of processing time
private const double Mttf = 300.0; // Mean time to failure in minutes
private static readonly NormalTime ProcessingTime = N(TimeSpan.FromMinutes(10.0), TimeSpan.FromMinutes(2.0)); // Processing time distribution
private static readonly ExponentialTime Failure = EXP(TimeSpan.FromMinutes(300.0)); // Failure distribution
private const double RepairTime = 30.0; // Time it takes to repair a machine in minutes
private const double JobDuration = 30.0; // Duration of other jobs in minutes
private const int NumMachines = 10; // Number of machines in the machine shop
Expand Down Expand Up @@ -77,7 +77,7 @@ private IEnumerable<Event> Working(PreemptiveResource repairman) {
*/
while (true) {
// Start making a new part
var doneIn = TimeSpan.FromMinutes(Environment.RandNormal(PtMean, PtSigma));
var doneIn = Environment.Rand(ProcessingTime);
while (doneIn > TimeSpan.Zero) {
// Working on the part
var start = Environment.Now;
Expand All @@ -104,7 +104,7 @@ private IEnumerable<Event> Working(PreemptiveResource repairman) {
private IEnumerable<Event> BreakMachine() {
// Break the machine every now and then.
while (true) {
yield return Environment.Timeout(TimeSpan.FromMinutes(Environment.RandExponential(Mttf)));
yield return Environment.Timeout(Failure);
if (!Broken) {
// Only break the machine if it is currently working.
Process.Interrupt();
Expand Down
4 changes: 3 additions & 1 deletion src/Benchmark/SyntheticBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Timers;
using static SimSharp.Distributions;

namespace SimSharp.Benchmarks {
public class SyntheticBenchmark {
Expand Down Expand Up @@ -106,8 +107,9 @@ static long Benchmark1(Simulation env, int n) {
}

static IEnumerable<Event> Benchmark1Proc(Simulation env, int n) {
var dist = UNIF(TimeSpan.Zero, TimeSpan.FromSeconds(2 * n));
while (true) {
yield return env.TimeoutUniform(TimeSpan.Zero, TimeSpan.FromSeconds(2 * n));
yield return env.Timeout(dist);
perf++;
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/Samples/BankRenege.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@

using System;
using System.Collections.Generic;
using static SimSharp.Distributions;

namespace SimSharp.Samples {
public class BankRenege {

private const int NewCustomers = 10; // Total number of customers
private static readonly TimeSpan IntervalCustomers = TimeSpan.FromMinutes(10.0); // Generate new customers roughly every x minutes
private static readonly TimeSpan MinPatience = TimeSpan.FromMinutes(1); // Min. customer patience
private static readonly TimeSpan MaxPatience = TimeSpan.FromMinutes(3); // Max. customer patience
private static readonly IDistribution<TimeSpan> Arrival = EXP(TimeSpan.FromMinutes(10.0)); // Generate new customers roughly every x minutes
private static readonly IDistribution<TimeSpan> Patience = UNIF(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(3)); // Customer patience

private IEnumerable<Event> Source(Simulation env, Resource counter) {
for (int i = 0; i < NewCustomers; i++) {
var c = Customer(env, "Customer " + i, counter, TimeSpan.FromMinutes(12.0));
var c = Customer(env, "Customer " + i, counter, EXP(TimeSpan.FromMinutes(12.0)));
env.Process(c);
yield return env.TimeoutExponential(IntervalCustomers);
yield return env.Timeout(Arrival);
}
}

private IEnumerable<Event> Customer(Simulation env, string name, Resource counter, TimeSpan meanTimeInBank) {
private IEnumerable<Event> Customer(Simulation env, string name, Resource counter, IDistribution<TimeSpan> meanTimeInBank) {
var arrive = env.Now;

env.Log("{0} {1}: Here I am", arrive, name);

using (var req = counter.Request()) {
// Wait for the counter or abort at the end of our tether
var timeout = env.TimeoutUniform(MinPatience, MaxPatience);
var timeout = env.Timeout(Patience);
yield return req | timeout;

var wait = env.Now - arrive;
Expand All @@ -40,7 +40,7 @@ private IEnumerable<Event> Customer(Simulation env, string name, Resource counte
// We got the counter
env.Log("{0} {1}: waited {2}", env.Now, name, wait);

yield return env.TimeoutExponential(meanTimeInBank);
yield return env.Timeout(meanTimeInBank);
env.Log("{0} {1}: Finished", env.Now, name);
} else {
// We reneged
Expand Down
11 changes: 5 additions & 6 deletions src/Samples/GasStationRefueling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using System;
using System.Collections.Generic;
using static SimSharp.Distributions;

namespace SimSharp.Samples {
public class GasStationRefueling {
Expand All @@ -31,12 +32,10 @@ public class GasStationRefueling {
private const int GasStationSize = 200; // liters
private const int Threshold = 10; // Threshold for calling the tank truck (in %)
private const int FuelTankSize = 50; // liters
private const int MinFuelTankLevel = 5; // Min levels of fuel tanks (in liters)
private const int MaxFuelTankLevel = 25; // Max levels of fuel tanks (in liters)
private const int RefuelingSpeed = 2; // liters / second
private static readonly Uniform InitialFuelLevel = UNIF(5, 26); // Level of fuel tanks (in liters)
private static readonly TimeSpan TankTruckTime = TimeSpan.FromMinutes(10); // Minutes it takes the tank truck to arrive
private static readonly TimeSpan MinTInter = TimeSpan.FromMinutes(30); // Create a car every min seconds
private static readonly TimeSpan MaxTInter = TimeSpan.FromMinutes(300); // Create a car every max seconds
private static readonly UniformTime CarArrival = UNIF(TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(300)); // Arrival distribution for cars
private static readonly TimeSpan SimTime = TimeSpan.FromMinutes(3000); // Simulation time

private IEnumerable<Event> Car(string name, Simulation env, Resource gasStation, Container fuelPump) {
Expand All @@ -47,7 +46,7 @@ private IEnumerable<Event> Car(string name, Simulation env, Resource gasStation,
* desired amount of gas from it. If the stations reservoir is
* depleted, the car has to wait for the tank truck to arrive.
*/
var fuelTankLevel = env.RandUniform(MinFuelTankLevel, MaxFuelTankLevel + 1);
var fuelTankLevel = env.Rand(InitialFuelLevel);
env.Log("{0} arriving at gas station at {1}", name, env.Now);
using (var req = gasStation.Request()) {
var start = env.Now;
Expand Down Expand Up @@ -98,7 +97,7 @@ private IEnumerable<Event> CarGenerator(Simulation env, Resource gasStation, Con
var i = 0;
while (true) {
i++;
yield return env.Timeout(env.RandUniform(MinTInter, MaxTInter));
yield return env.Timeout(CarArrival);
env.Process(Car("Car " + i, env, gasStation, fuelPump));
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/Samples/KanbanControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@

using System;
using System.Collections.Generic;
using static SimSharp.Distributions;

namespace SimSharp.Samples {
public class KanbanControl {
private Simulation env;
private Resource kanban;
private Resource server;
private TimeSeriesMonitor stockStat;
private static readonly TimeSpan OrderArrivalTime = TimeSpan.FromMinutes(3.33);
private static readonly TimeSpan ProcessingTime = TimeSpan.FromMinutes(2.5);
private static readonly ExponentialTime OrderArrival = EXP(TimeSpan.FromMinutes(3.33));
private static readonly ExponentialTime ProcessingTime = EXP(TimeSpan.FromMinutes(2.5));
private int completedOrders;

private IEnumerable<Event> Source() {
while (true) {
yield return env.TimeoutExponential(OrderArrivalTime);
yield return env.Timeout(OrderArrival);
env.Process(Order());
}
}
Expand All @@ -36,7 +37,7 @@ private IEnumerable<Event> Order() {
private IEnumerable<Event> Produce(Request kb) {
using (var srv = server.Request()) {
yield return srv;
yield return env.TimeoutExponential(ProcessingTime);
yield return env.Timeout(ProcessingTime);
kanban.Release(kb);
stockStat.UpdateTo(kanban.Remaining);
}
Expand Down
13 changes: 7 additions & 6 deletions src/Samples/MM1Queueing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@

using System;
using System.Collections.Generic;
using static SimSharp.Distributions;

namespace SimSharp.Samples {
public class MM1Queueing {
private static readonly TimeSpan OrderArrivalTime = TimeSpan.FromMinutes(3.33);
private static readonly TimeSpan ProcessingTime = TimeSpan.FromMinutes(2.5);
private static readonly ExponentialTime OrderArrival = EXP(TimeSpan.FromMinutes(3.33));
private static readonly ExponentialTime ProcessingTime = EXP(TimeSpan.FromMinutes(2.5));

private IEnumerable<Event> Source(Simulation env, Resource server) {
while (true) {
yield return env.TimeoutExponential(OrderArrivalTime);
yield return env.Timeout(OrderArrival);
env.Process(Order(env, server));
}
}

private IEnumerable<Event> Order(Simulation env, Resource server) {
using (var req = server.Request()) {
yield return req;
yield return env.TimeoutExponential(ProcessingTime);
yield return env.Timeout(ProcessingTime);
}
}

Expand All @@ -34,8 +35,8 @@ private IEnumerable<Event> HandleWarmup(Simulation env, TimeSpan warmupTime, par
}

public void Simulate(int repetitions = 5) {
var lambda = 1 / OrderArrivalTime.TotalDays;
var mu = 1 / ProcessingTime.TotalDays;
var lambda = 1 / OrderArrival.Mean.TotalDays;
var mu = 1 / ProcessingTime.Mean.TotalDays;
var rho = lambda / mu;
var analyticWIP = rho / (1 - rho);
var analyticLeadtime = 1 / (mu - lambda);
Expand Down
Loading

0 comments on commit a02e6d2

Please sign in to comment.