sim-tools 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sim_tools-0.4.0 → sim_tools-0.6.0}/PKG-INFO +32 -11
- {sim_tools-0.4.0 → sim_tools-0.6.0}/README.md +31 -10
- sim_tools-0.6.0/sim_tools/__init__.py +4 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/distributions.py +26 -30
- sim_tools-0.6.0/sim_tools/time_dependent.py +266 -0
- sim_tools-0.6.0/sim_tools/trace.py +182 -0
- sim_tools-0.4.0/sim_tools/__init__.py +0 -4
- sim_tools-0.4.0/sim_tools/time_dependent.py +0 -100
- {sim_tools-0.4.0 → sim_tools-0.6.0}/.gitignore +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/LICENSE +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/pyproject.toml +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/data/nspp_example1.csv +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/datasets.py +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/ovs/__init__.py +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/ovs/evaluation.py +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/ovs/fixed_budget.py +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/ovs/indifference_zone.py +0 -0
- {sim_tools-0.4.0 → sim_tools-0.6.0}/sim_tools/ovs/toy_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sim-tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Simulation Tools for Education and Practice
|
|
5
5
|
Project-URL: Homepage, https://github.com/TomMonks/sim-tools
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TomMonks/sim-tools/issues
|
|
@@ -20,35 +20,56 @@ Requires-Dist: scikit-learn>=1.0.0
|
|
|
20
20
|
Requires-Dist: scipy>=1.4.1
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
|
-
# sim-tools
|
|
23
|
+
# `sim-tools`: tools to support the Discrete-Event Simulation process in python.
|
|
24
24
|
|
|
25
25
|
[](https://mybinder.org/v2/gh/TomMonks/sim-tools/HEAD)
|
|
26
26
|
[](https://zenodo.org/badge/latestdoi/225608065)
|
|
27
27
|
[](https://pypi.python.org/pypi/sim-tools/)
|
|
28
|
+
[](https://anaconda.org/conda-forge/sim-tools)
|
|
29
|
+
[](https://anaconda.org/conda-forge/sim-tools)
|
|
28
30
|
[](https://tommonks.github.io/sim-tools)
|
|
29
31
|
[](https://opensource.org/licenses/MIT)
|
|
30
32
|
[](https://www.python.org/downloads/release/python-360+/)
|
|
31
33
|
[](https://orcid.org/0000-0003-2631-4481)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
`sim-tools` is being developed to support Discrete-Event Simulation (DES) education and applied simulation research. It is MIT licensed and freely available to practitioners, students and researchers via [PyPi](https://pypi.org/project/sim-tools/) and [conda-forge](https://anaconda.org/conda-forge/sim-tools)
|
|
34
37
|
|
|
35
38
|
# Vision for sim-tools
|
|
36
39
|
|
|
37
|
-
1. Deliver high quality reliable code for
|
|
40
|
+
1. Deliver high quality reliable code for DES education and practice with full documentation.
|
|
38
41
|
2. Provide a simple to use pythonic interface.
|
|
39
|
-
3. To improve the quality of
|
|
42
|
+
3. To improve the quality of DES education using FOSS tools and encourage the use of best practice.
|
|
40
43
|
|
|
41
44
|
# Features:
|
|
42
45
|
|
|
43
|
-
1. Implementation of classic
|
|
46
|
+
1. Implementation of classic Optimisation via Simulation procedures such as KN, KN++, OBCA and OBCA-m
|
|
44
47
|
2. Distributions module that includes classes that encapsulate a random number stream, seed, and distribution parameters.
|
|
45
|
-
3. Implementation of Thinning to sample from Non-stationary poisson processes in a
|
|
48
|
+
3. Implementation of Thinning to sample from Non-stationary poisson processes in a DES.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
### Pip and PyPi
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install sim-tools
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conda-forge
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
conda install -c conda-forge sim-tools
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Binder
|
|
65
|
+
|
|
66
|
+
[](https://mybinder.org/v2/gh/TomMonks/sim-tools/HEAD)
|
|
67
|
+
|
|
46
68
|
|
|
47
|
-
##
|
|
69
|
+
## Learn how to use `sim-tools`
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
3. Oneline documentation: https://tommonks.github.io/sim-tools
|
|
71
|
+
* Online documentation: https://tommonks.github.io/sim-tools
|
|
72
|
+
* Introduction to DES in python: https://health-data-science-or.github.io/simpy-streamlit-tutorial/
|
|
52
73
|
|
|
53
74
|
## Citation
|
|
54
75
|
|
|
@@ -1,32 +1,53 @@
|
|
|
1
|
-
# sim-tools
|
|
1
|
+
# `sim-tools`: tools to support the Discrete-Event Simulation process in python.
|
|
2
2
|
|
|
3
3
|
[](https://mybinder.org/v2/gh/TomMonks/sim-tools/HEAD)
|
|
4
4
|
[](https://zenodo.org/badge/latestdoi/225608065)
|
|
5
5
|
[](https://pypi.python.org/pypi/sim-tools/)
|
|
6
|
+
[](https://anaconda.org/conda-forge/sim-tools)
|
|
7
|
+
[](https://anaconda.org/conda-forge/sim-tools)
|
|
6
8
|
[](https://tommonks.github.io/sim-tools)
|
|
7
9
|
[](https://opensource.org/licenses/MIT)
|
|
8
10
|
[](https://www.python.org/downloads/release/python-360+/)
|
|
9
11
|
[](https://orcid.org/0000-0003-2631-4481)
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
`sim-tools` is being developed to support Discrete-Event Simulation (DES) education and applied simulation research. It is MIT licensed and freely available to practitioners, students and researchers via [PyPi](https://pypi.org/project/sim-tools/) and [conda-forge](https://anaconda.org/conda-forge/sim-tools)
|
|
12
15
|
|
|
13
16
|
# Vision for sim-tools
|
|
14
17
|
|
|
15
|
-
1. Deliver high quality reliable code for
|
|
18
|
+
1. Deliver high quality reliable code for DES education and practice with full documentation.
|
|
16
19
|
2. Provide a simple to use pythonic interface.
|
|
17
|
-
3. To improve the quality of
|
|
20
|
+
3. To improve the quality of DES education using FOSS tools and encourage the use of best practice.
|
|
18
21
|
|
|
19
22
|
# Features:
|
|
20
23
|
|
|
21
|
-
1. Implementation of classic
|
|
24
|
+
1. Implementation of classic Optimisation via Simulation procedures such as KN, KN++, OBCA and OBCA-m
|
|
22
25
|
2. Distributions module that includes classes that encapsulate a random number stream, seed, and distribution parameters.
|
|
23
|
-
3. Implementation of Thinning to sample from Non-stationary poisson processes in a
|
|
26
|
+
3. Implementation of Thinning to sample from Non-stationary poisson processes in a DES.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### Pip and PyPi
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install sim-tools
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Conda-forge
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
conda install -c conda-forge sim-tools
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Binder
|
|
43
|
+
|
|
44
|
+
[](https://mybinder.org/v2/gh/TomMonks/sim-tools/HEAD)
|
|
45
|
+
|
|
24
46
|
|
|
25
|
-
##
|
|
47
|
+
## Learn how to use `sim-tools`
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
3. Oneline documentation: https://tommonks.github.io/sim-tools
|
|
49
|
+
* Online documentation: https://tommonks.github.io/sim-tools
|
|
50
|
+
* Introduction to DES in python: https://health-data-science-or.github.io/simpy-streamlit-tutorial/
|
|
30
51
|
|
|
31
52
|
## Citation
|
|
32
53
|
|
|
@@ -169,70 +169,66 @@ class Lognormal(Distribution):
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
class Normal(Distribution):
|
|
172
|
-
|
|
172
|
+
'''
|
|
173
173
|
Convenience class for the normal distribution.
|
|
174
174
|
packages up distribution parameters, seed and random generator.
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"""
|
|
179
|
-
|
|
176
|
+
Use the minimum parameter to truncate the distribution
|
|
177
|
+
'''
|
|
180
178
|
def __init__(
|
|
181
179
|
self,
|
|
182
180
|
mean: float,
|
|
183
181
|
sigma: float,
|
|
184
|
-
|
|
182
|
+
minimum: Optional[float] = None,
|
|
185
183
|
random_seed: Optional[int] = None,
|
|
186
184
|
):
|
|
187
|
-
|
|
185
|
+
'''
|
|
188
186
|
Constructor
|
|
189
|
-
|
|
187
|
+
|
|
190
188
|
Params:
|
|
191
189
|
------
|
|
192
190
|
mean: float
|
|
193
191
|
The mean of the normal distribution
|
|
194
|
-
|
|
192
|
+
|
|
195
193
|
sigma: float
|
|
196
194
|
The stdev of the normal distribution
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
minimum: float
|
|
197
|
+
Truncate the normal distribution to a minimum
|
|
198
|
+
value.
|
|
199
|
+
|
|
202
200
|
random_seed: int, optional (default=None)
|
|
203
201
|
A random seed to reproduce samples. If set to none then a unique
|
|
204
202
|
sample is created.
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
'''
|
|
204
|
+
self.rng = np.random.default_rng(seed=random_seed)
|
|
207
205
|
self.mean = mean
|
|
208
206
|
self.sigma = sigma
|
|
209
|
-
self.
|
|
210
|
-
|
|
207
|
+
self.minimum = minimum
|
|
208
|
+
|
|
211
209
|
def sample(self, size: Optional[int] = None) -> float | np.ndarray:
|
|
212
|
-
|
|
210
|
+
'''
|
|
213
211
|
Generate a sample from the normal distribution
|
|
214
|
-
|
|
212
|
+
|
|
215
213
|
Params:
|
|
216
214
|
-------
|
|
217
215
|
size: int, optional (default=None)
|
|
218
216
|
the number of samples to return. If size=None then a single
|
|
219
217
|
sample is returned.
|
|
220
|
-
|
|
221
|
-
# initial sample
|
|
218
|
+
'''
|
|
222
219
|
samples = self.rng.normal(self.mean, self.sigma, size=size)
|
|
223
220
|
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
if self.minimum is None:
|
|
222
|
+
return samples
|
|
223
|
+
elif size is None:
|
|
224
|
+
return max(self.minimum, samples)
|
|
225
|
+
else:
|
|
226
|
+
# index of samples with negative value
|
|
227
|
+
neg_idx = np.where(samples < 0)[0]
|
|
228
|
+
samples[neg_idx] = self.minimum
|
|
226
229
|
return samples
|
|
227
230
|
|
|
228
|
-
# repeatedly resample negative values
|
|
229
|
-
negs = np.where(samples < 0)[0]
|
|
230
|
-
while len(negs) > 0:
|
|
231
|
-
resample = self.rng.normal(self.mean, self.sigma, size=len(negs))
|
|
232
|
-
samples[negs] = resample
|
|
233
|
-
negs = np.where(samples < 0)[0]
|
|
234
231
|
|
|
235
|
-
return samples
|
|
236
232
|
|
|
237
233
|
|
|
238
234
|
class Uniform(Distribution):
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes and functions to support time dependent samplingm in DES models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NSPPThinning:
|
|
14
|
+
"""
|
|
15
|
+
Non Stationary Poisson Process via Thinning.
|
|
16
|
+
|
|
17
|
+
Thinning is an acceptance-rejection approach to sampling
|
|
18
|
+
inter-arrival times (IAT) from a time dependent distribution
|
|
19
|
+
where each time period follows its own exponential distribution.
|
|
20
|
+
|
|
21
|
+
There are two random variables employed in sampling: an exponential
|
|
22
|
+
distribution (used to sample IAT) and a uniform distibution (used
|
|
23
|
+
to accept/reject samples).
|
|
24
|
+
|
|
25
|
+
All IATs are sampled from an Exponential distribution with the highest
|
|
26
|
+
arrival rate (most frequent). These arrivals are then rejected (thinned)
|
|
27
|
+
proportional to the ratio of the current arrival rate to the maximum
|
|
28
|
+
arrival rate. The algorithm executes until a sample is accepted. The IAT
|
|
29
|
+
returned is the sum of all the IATs that were sampled.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
data,
|
|
36
|
+
random_seed1: Optional[int] = None,
|
|
37
|
+
random_seed2: Optional[int] = None,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Non Stationary Poisson Process via Thinning.
|
|
41
|
+
|
|
42
|
+
Time dependency is handled for a single table
|
|
43
|
+
consisting of equally spaced intervals.
|
|
44
|
+
|
|
45
|
+
Params:
|
|
46
|
+
------
|
|
47
|
+
data: pandas.DataFrame
|
|
48
|
+
list of time points during a period for transition between rates
|
|
49
|
+
and list arrival rates in that period. Labels should be "t"
|
|
50
|
+
and "arrival_rate" respectively.
|
|
51
|
+
|
|
52
|
+
random_seed1: int, optional (default=None)
|
|
53
|
+
Random seed for exponential distribution
|
|
54
|
+
|
|
55
|
+
random_seed2: int
|
|
56
|
+
Random seed for the uniform distribution used
|
|
57
|
+
for acceptance/rejection sampling.
|
|
58
|
+
"""
|
|
59
|
+
self.data = data
|
|
60
|
+
self.arr_rng = np.random.default_rng(random_seed1)
|
|
61
|
+
self.thinning_rng = np.random.default_rng(random_seed2)
|
|
62
|
+
self.lambda_max = data["arrival_rate"].max()
|
|
63
|
+
self.min_iat = data["mean_iat"].min()
|
|
64
|
+
# assumes all other intervals are equal in length.
|
|
65
|
+
self.interval = int(data.iloc[1]["t"] - data.iloc[0]["t"])
|
|
66
|
+
self.rejects_last_sample = None
|
|
67
|
+
|
|
68
|
+
def sample(self, simulation_time: float) -> float:
|
|
69
|
+
"""
|
|
70
|
+
Run a single iteration of acceptance-rejection
|
|
71
|
+
thinning alg to sample the next inter-arrival time
|
|
72
|
+
|
|
73
|
+
Params:
|
|
74
|
+
------
|
|
75
|
+
simulation_time: float
|
|
76
|
+
The current simulation time. This is used to look up
|
|
77
|
+
the arrival rate for the time period.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
-------
|
|
81
|
+
float
|
|
82
|
+
The inter-arrival time
|
|
83
|
+
"""
|
|
84
|
+
for _ in itertools.count():
|
|
85
|
+
# this gives us the index of dataframe to use
|
|
86
|
+
t = int(simulation_time // self.interval) % len(self.data)
|
|
87
|
+
lambda_t = self.data["arrival_rate"].iloc[t]
|
|
88
|
+
|
|
89
|
+
# set to a large number so that at least 1 sample taken!
|
|
90
|
+
u = np.inf
|
|
91
|
+
|
|
92
|
+
# included for audit and tracking purposes.
|
|
93
|
+
self.rejects_last_sample = 0.0
|
|
94
|
+
|
|
95
|
+
interarrival_time = 0.0
|
|
96
|
+
|
|
97
|
+
# reject samples if u >= lambda_t / lambda_max
|
|
98
|
+
while u >= (lambda_t / self.lambda_max):
|
|
99
|
+
self.rejects_last_sample += 1
|
|
100
|
+
interarrival_time += self.arr_rng.exponential(self.min_iat)
|
|
101
|
+
u = self.thinning_rng.uniform(0.0, 1.0)
|
|
102
|
+
|
|
103
|
+
return interarrival_time
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def nspp_simulation(
|
|
107
|
+
arrival_profile: pd.DataFrame,
|
|
108
|
+
run_length: Optional[float] = None,
|
|
109
|
+
n_reps: Optional[int] = 1000,
|
|
110
|
+
) -> pd.DataFrame:
|
|
111
|
+
"""
|
|
112
|
+
Generate a pandas dataframe that contains multiple replications of
|
|
113
|
+
a non-stationary poisson process for the set arrival profile.
|
|
114
|
+
|
|
115
|
+
This uses the sim-tools NSPPThinning class.
|
|
116
|
+
|
|
117
|
+
Useful for validating the the NSPP has been set up correctly and is producing the
|
|
118
|
+
desired profile for the simulation model.
|
|
119
|
+
|
|
120
|
+
On each replication the function counts the number of arrivals during the intervals
|
|
121
|
+
from the arrival profile. Returns a data frame with reps (rows) and interval arrivals
|
|
122
|
+
(columns)
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
-----------
|
|
126
|
+
arrival_profile: pandas.DataFrame
|
|
127
|
+
The arrival profile is a pandas data frame containing 't', 'arrival_rate' and
|
|
128
|
+
'mean_iat' columns.
|
|
129
|
+
|
|
130
|
+
run_length: float, optional (default=None)
|
|
131
|
+
How long should the simulation be run. If none then uses the last value in 't'
|
|
132
|
+
+ the interval (assumes equal width intervals)
|
|
133
|
+
|
|
134
|
+
n_reps: int, optional (default=1000)
|
|
135
|
+
The number of replications to run.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
--------
|
|
139
|
+
pd.DataFrame.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
# replication results
|
|
144
|
+
replication_results = []
|
|
145
|
+
|
|
146
|
+
# multiple replications
|
|
147
|
+
for rep in range(n_reps):
|
|
148
|
+
|
|
149
|
+
# method for producing n non-overlapping streams
|
|
150
|
+
seed_sequence = np.random.SeedSequence(rep)
|
|
151
|
+
|
|
152
|
+
# Generate n high quality child seeds
|
|
153
|
+
seeds = seed_sequence.spawn(2)
|
|
154
|
+
|
|
155
|
+
# create nspp
|
|
156
|
+
nspp_rng = NSPPThinning(arrival_profile, seeds[0], seeds[1])
|
|
157
|
+
|
|
158
|
+
# if no run length has been set....
|
|
159
|
+
if run_length is None:
|
|
160
|
+
run_length = (
|
|
161
|
+
arrival_profile["t"].iloc[len(arrival_profile) - 1] + nspp_rng.interval
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# list - each item is an interval in the arrival profile
|
|
165
|
+
interval_samples = [0] * arrival_profile.shape[0]
|
|
166
|
+
simulation_time = 0.0
|
|
167
|
+
while simulation_time < run_length:
|
|
168
|
+
iat = nspp_rng.sample(simulation_time)
|
|
169
|
+
simulation_time += iat
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if simulation_time < run_length:
|
|
173
|
+
# data collection: add one to count for hour of the day
|
|
174
|
+
# note list NSPPThinning this assume equal intervals
|
|
175
|
+
interval_of_day = (
|
|
176
|
+
int(simulation_time // nspp_rng.interval) % len(arrival_profile)
|
|
177
|
+
)
|
|
178
|
+
interval_samples[interval_of_day] += 1
|
|
179
|
+
|
|
180
|
+
replication_results.append(interval_samples)
|
|
181
|
+
|
|
182
|
+
# produce summary chart of arrivals per interval
|
|
183
|
+
# format in a dataframe
|
|
184
|
+
df_replications = pd.DataFrame(replication_results)
|
|
185
|
+
df_replications.index = np.arange(1, len(df_replications) + 1)
|
|
186
|
+
df_replications.index.name = "rep"
|
|
187
|
+
|
|
188
|
+
return df_replications
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def nspp_plot(
|
|
192
|
+
arrival_profile: pd.DataFrame,
|
|
193
|
+
run_length: Optional[float] = None,
|
|
194
|
+
n_reps: Optional[int] = 1000,
|
|
195
|
+
) -> Tuple[plt.Figure, plt.Axes]:
|
|
196
|
+
"""Generate a matplotlib chart to visualise a non-stationary poisson process
|
|
197
|
+
for the set arrival profile.
|
|
198
|
+
|
|
199
|
+
This uses the sim-tools NSPPThinning class.
|
|
200
|
+
|
|
201
|
+
Useful for validating the the NSPP has been set up correctly and is producing the
|
|
202
|
+
desired profile for the simulation model.
|
|
203
|
+
|
|
204
|
+
Parameters:
|
|
205
|
+
----------
|
|
206
|
+
arrival_profile: pandas.DataFrame
|
|
207
|
+
The arrival profile is a pandas data frame containing 't', 'arrival_rate' and
|
|
208
|
+
'mean_iat' columns.
|
|
209
|
+
|
|
210
|
+
run_length: float, optional (default=None)
|
|
211
|
+
How long should the simulation be run. If none then uses the last value in 't'
|
|
212
|
+
+ the interval (assumes equal width intervals)
|
|
213
|
+
|
|
214
|
+
n_reps: int, optional (default=1000)
|
|
215
|
+
The number of replications to run.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
# verification of arrival_profile
|
|
219
|
+
|
|
220
|
+
# is it a dataframe
|
|
221
|
+
if not isinstance(arrival_profile, pd.DataFrame):
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"arrival_profile expected pd.DataFrame " f"got {type(arrival_profile)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# all columns are present
|
|
227
|
+
required_columns = ["t", "arrival_rate", "mean_iat"]
|
|
228
|
+
for col in required_columns:
|
|
229
|
+
if col not in arrival_profile.columns:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"arrival_profile must contain "
|
|
232
|
+
f"the following columns: {required_columns}. "
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# generate the sample data
|
|
236
|
+
df_interval_results = nspp_simulation(arrival_profile, run_length, n_reps)
|
|
237
|
+
|
|
238
|
+
interval_means = df_interval_results.mean(axis=0)
|
|
239
|
+
interval_sd = df_interval_results.std(axis=0)
|
|
240
|
+
|
|
241
|
+
upper = interval_means + interval_sd
|
|
242
|
+
lower = interval_means - interval_sd
|
|
243
|
+
lower[lower < 0] = 0
|
|
244
|
+
|
|
245
|
+
# visualise
|
|
246
|
+
fig = plt.figure(figsize=(12, 3))
|
|
247
|
+
ax = fig.add_subplot()
|
|
248
|
+
|
|
249
|
+
# chart x ticks
|
|
250
|
+
x_values = np.arange(0, arrival_profile.shape[0])
|
|
251
|
+
|
|
252
|
+
# plot in this case returns a 2D line plot object
|
|
253
|
+
_ = ax.plot(arrival_profile["t"], interval_means, label="Mean")
|
|
254
|
+
_ = ax.fill_between(arrival_profile["t"], lower, upper, alpha=0.2, label="+-1SD")
|
|
255
|
+
|
|
256
|
+
# chart appearance
|
|
257
|
+
_ = ax.legend(loc="best", ncol=3)
|
|
258
|
+
_ = ax.set_ylim(
|
|
259
|
+
0,
|
|
260
|
+
)
|
|
261
|
+
_ = ax.set_xlim(0, arrival_profile.shape[0] - 1)
|
|
262
|
+
_ = ax.set_ylabel("arrivals")
|
|
263
|
+
_ = ax.set_xlabel("interval (from profile)")
|
|
264
|
+
_ = plt.xticks(arrival_profile["t"])
|
|
265
|
+
|
|
266
|
+
return fig, ax
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple functionality aiming to enhanced a users a
|
|
3
|
+
ability to trace and debug simulation models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
DEFAULT_DEBUG = False
|
|
11
|
+
|
|
12
|
+
CONFIG_ERROR = ("Your trace has not been initialised. "
|
|
13
|
+
"Call super__init__(debug=True) in class initialiser"
|
|
14
|
+
"or omit debug for default of no trace.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## single rich console - module level.
|
|
18
|
+
_console = Console()
|
|
19
|
+
|
|
20
|
+
class Traceable(ABC):
|
|
21
|
+
'''Provides basic trace functionality for a process to subclass.
|
|
22
|
+
|
|
23
|
+
Abstract base class Traceable
|
|
24
|
+
|
|
25
|
+
Subclasses must call
|
|
26
|
+
|
|
27
|
+
super().__init__(debug=True) in their __init__() method to
|
|
28
|
+
initialise trace.
|
|
29
|
+
|
|
30
|
+
Subclasses inherit the following methods:
|
|
31
|
+
|
|
32
|
+
trace() - use this function print out a traceable event
|
|
33
|
+
|
|
34
|
+
_trace_config(): use this function to return a dict containing
|
|
35
|
+
the trace configuration for the class. Subclasses should
|
|
36
|
+
override it to implement custom formatting.
|
|
37
|
+
|
|
38
|
+
Notes:
|
|
39
|
+
-----
|
|
40
|
+
This class provides the same functionality as the function `trace()`
|
|
41
|
+
in an object orientated framework. It in theory means cleaner code
|
|
42
|
+
as the call to trace requires less parameters. However, it must
|
|
43
|
+
be setup correctly.
|
|
44
|
+
'''
|
|
45
|
+
def __init__(self, debug: Optional[bool] = DEFAULT_DEBUG):
|
|
46
|
+
"""Initialise Traceable
|
|
47
|
+
|
|
48
|
+
Parameters:
|
|
49
|
+
----------
|
|
50
|
+
debug: bool, Optional (default=False)
|
|
51
|
+
show trace(True). do not show trace (False)
|
|
52
|
+
"""
|
|
53
|
+
self.debug = debug
|
|
54
|
+
self._config = Traceable._default_config()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _default_config(cls) -> dict:
|
|
58
|
+
"""Returns a default trace configuration"""
|
|
59
|
+
config = {
|
|
60
|
+
"class":None,
|
|
61
|
+
"class_colour":"bold blue",
|
|
62
|
+
"time_colour":'bold blue',
|
|
63
|
+
"time_dp":2,
|
|
64
|
+
"message_colour":'black',
|
|
65
|
+
"tracked":None
|
|
66
|
+
}
|
|
67
|
+
return config
|
|
68
|
+
|
|
69
|
+
def _trace_config(self) -> dict:
|
|
70
|
+
"""Overload to return a custom trace configuration"""
|
|
71
|
+
return Traceable._default_config()
|
|
72
|
+
|
|
73
|
+
def trace(self, time: float, msg: Optional[str] = None, process_id: Optional[str] = None):
|
|
74
|
+
'''Display a formatted trace of a simulated event.
|
|
75
|
+
|
|
76
|
+
Implemented with the rich library Console() object.
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
----------
|
|
80
|
+
time: float
|
|
81
|
+
The simulation time
|
|
82
|
+
|
|
83
|
+
msg: str, Optional (default=None)
|
|
84
|
+
Event message to display to user
|
|
85
|
+
|
|
86
|
+
process_id: str, Optional (default=None)
|
|
87
|
+
Display an unique identifer for the trace message
|
|
88
|
+
|
|
89
|
+
'''
|
|
90
|
+
|
|
91
|
+
# did not initialise trace
|
|
92
|
+
if not hasattr(self, '_config'):
|
|
93
|
+
raise AttributeError(CONFIG_ERROR)
|
|
94
|
+
|
|
95
|
+
# if in debug mode
|
|
96
|
+
if self.debug:
|
|
97
|
+
|
|
98
|
+
# check for override to default configs
|
|
99
|
+
process_config = self._trace_config()
|
|
100
|
+
self._config.update(process_config)
|
|
101
|
+
|
|
102
|
+
# conditional logic to limit tracking to specific processes/entities
|
|
103
|
+
if self._config['tracked'] is None or process_id in self._config['tracked']:
|
|
104
|
+
|
|
105
|
+
# display and format time stamp
|
|
106
|
+
out = f"[{self._config['time_colour']}][{time:.{self._config['time_dp']}f}]:[/{self._config['time_colour']}]"
|
|
107
|
+
|
|
108
|
+
# if provided display and format a process ID
|
|
109
|
+
if self._config['class'] is not None and process_id is not None:
|
|
110
|
+
out += f"[{self._config['class_colour']}]<{self._config['class']} {process_id}>: [/{self._config['class_colour']}]"
|
|
111
|
+
|
|
112
|
+
# format traced event message
|
|
113
|
+
out += f"[{self._config['message_colour']}]{msg}[/{self._config['message_colour']}]"
|
|
114
|
+
|
|
115
|
+
# print to rich console
|
|
116
|
+
_console.print(out)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def trace(time: float, debug: Optional[bool] = DEFAULT_DEBUG, msg: Optional[str] = None,
|
|
120
|
+
identifier: Optional[str] = None, config: Optional[dict] = None):
|
|
121
|
+
"""Display a formatted trace of a simulated event.
|
|
122
|
+
|
|
123
|
+
Implemented with the rich library Console() object.
|
|
124
|
+
|
|
125
|
+
Parameters:
|
|
126
|
+
----------
|
|
127
|
+
time: float
|
|
128
|
+
The simulation time
|
|
129
|
+
|
|
130
|
+
debug: bool, Optional (default=False)
|
|
131
|
+
show trace(True). do not show trace (False)
|
|
132
|
+
|
|
133
|
+
msg: str, Optional (default=None)
|
|
134
|
+
Event message to display to user
|
|
135
|
+
|
|
136
|
+
identifier: str, Optional (default=None)
|
|
137
|
+
Display an unique identifier for the trace message
|
|
138
|
+
|
|
139
|
+
config: dict, Optional (default=None)
|
|
140
|
+
If None then default colouring is applied to a message
|
|
141
|
+
Options (with corresponding defaults) include:
|
|
142
|
+
|
|
143
|
+
"name":None,
|
|
144
|
+
"name_colour":"bold blue",
|
|
145
|
+
"time_colour":'bold blue',
|
|
146
|
+
"time_dp":2,
|
|
147
|
+
"message_colour":'black',
|
|
148
|
+
"tracked":None
|
|
149
|
+
|
|
150
|
+
Use tracked to only show trace for specific process IDs.
|
|
151
|
+
For example if a process are labelled as integers from
|
|
152
|
+
1 to n and we wished to track processes 5, 6 and 25. Then we would set
|
|
153
|
+
tracked = [1, 6, 25]
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# get default and then update with user settings.
|
|
158
|
+
_config = Traceable._default_config()
|
|
159
|
+
if config is None:
|
|
160
|
+
_config['class'] = "event"
|
|
161
|
+
else:
|
|
162
|
+
# update with user settings.
|
|
163
|
+
_config.update(config)
|
|
164
|
+
|
|
165
|
+
# if in debug mode
|
|
166
|
+
if debug:
|
|
167
|
+
|
|
168
|
+
# conditional logic to limit tracking to specific processes/entities
|
|
169
|
+
if _config['tracked'] is None or identifier in _config['tracked']:
|
|
170
|
+
|
|
171
|
+
# display and format time stamp
|
|
172
|
+
out = f"[{_config['time_colour']}][{time:.{_config['time_dp']}f}]:[/{_config['time_colour']}]"
|
|
173
|
+
|
|
174
|
+
# if provided display and format a process ID
|
|
175
|
+
if _config['class'] is not None and identifier is not None:
|
|
176
|
+
out += f"[{_config['class_colour']}]<{_config['class']} {identifier}>: [/{_config['class_colour']}]"
|
|
177
|
+
|
|
178
|
+
# format traced event message
|
|
179
|
+
out += f"[{_config['message_colour']}]{msg}[/{_config['message_colour']}]"
|
|
180
|
+
|
|
181
|
+
# print to rich console
|
|
182
|
+
_console.print(out)
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Classes and functions to support time dependent samplingm in DES models.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import itertools
|
|
6
|
-
import numpy as np
|
|
7
|
-
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class NSPPThinning:
|
|
12
|
-
"""
|
|
13
|
-
Non Stationary Poisson Process via Thinning.
|
|
14
|
-
|
|
15
|
-
Thinning is an acceptance-rejection approach to sampling
|
|
16
|
-
inter-arrival times (IAT) from a time dependent distribution
|
|
17
|
-
where each time period follows its own exponential distribution.
|
|
18
|
-
|
|
19
|
-
There are two random variables employed in sampling: an exponential
|
|
20
|
-
distribution (used to sample IAT) and a uniform distibution (used
|
|
21
|
-
to accept/reject samples).
|
|
22
|
-
|
|
23
|
-
All IATs are sampled from an Exponential distribution with the highest
|
|
24
|
-
arrival rate (most frequent). These arrivals are then rejected (thinned)
|
|
25
|
-
proportional to the ratio of the current arrival rate to the maximum
|
|
26
|
-
arrival rate. The algorithm executes until a sample is accepted. The IAT
|
|
27
|
-
returned is the sum of all the IATs that were sampled.
|
|
28
|
-
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
data,
|
|
34
|
-
random_seed1: Optional[int] = None,
|
|
35
|
-
random_seed2: Optional[int] = None,
|
|
36
|
-
):
|
|
37
|
-
"""
|
|
38
|
-
Non Stationary Poisson Process via Thinning.
|
|
39
|
-
|
|
40
|
-
Time dependency is handled for a single table
|
|
41
|
-
consisting of equally spaced intervals.
|
|
42
|
-
|
|
43
|
-
Params:
|
|
44
|
-
------
|
|
45
|
-
data: pandas.DataFrame
|
|
46
|
-
list of time points during a period for transition between rates
|
|
47
|
-
and list arrival rates in that period. Labels should be "t"
|
|
48
|
-
and "arrival_rate" respectively.
|
|
49
|
-
|
|
50
|
-
random_seed1: int, optional (default=None)
|
|
51
|
-
Random seed for exponential distribution
|
|
52
|
-
|
|
53
|
-
random_seed2: int
|
|
54
|
-
Random seed for the uniform distribution used
|
|
55
|
-
for acceptance/rejection sampling.
|
|
56
|
-
"""
|
|
57
|
-
self.data = data
|
|
58
|
-
self.arr_rng = np.random.default_rng(random_seed1)
|
|
59
|
-
self.thinning_rng = np.random.default_rng(random_seed2)
|
|
60
|
-
self.lambda_max = data["arrival_rate"].max()
|
|
61
|
-
# assumes all other intervals are equal in length.
|
|
62
|
-
self.interval = int(data.iloc[1]["t"] - data.iloc[0]["t"])
|
|
63
|
-
self.rejects_last_sample = None
|
|
64
|
-
|
|
65
|
-
def sample(self, simulation_time: float) -> float:
|
|
66
|
-
"""
|
|
67
|
-
Run a single iteration of acceptance-rejection
|
|
68
|
-
thinning alg to sample the next inter-arrival time
|
|
69
|
-
|
|
70
|
-
Params:
|
|
71
|
-
------
|
|
72
|
-
simulation_time: float
|
|
73
|
-
The current simulation time. This is used to look up
|
|
74
|
-
the arrival rate for the time period.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
-------
|
|
78
|
-
float
|
|
79
|
-
The inter-arrival time
|
|
80
|
-
"""
|
|
81
|
-
for _ in itertools.count():
|
|
82
|
-
# this gives us the index of dataframe to use
|
|
83
|
-
t = int(simulation_time // self.interval) % len(self.data)
|
|
84
|
-
lambda_t = self.data["arrival_rate"].iloc[t]
|
|
85
|
-
|
|
86
|
-
# set to a large number so that at least 1 sample taken!
|
|
87
|
-
u = np.Inf
|
|
88
|
-
|
|
89
|
-
# included for audit and tracking purposes.
|
|
90
|
-
self.rejects_last_sample = 0.0
|
|
91
|
-
|
|
92
|
-
interarrival_time = 0.0
|
|
93
|
-
|
|
94
|
-
# reject samples if u >= lambda_t / lambda_max
|
|
95
|
-
while u >= (lambda_t / self.lambda_max):
|
|
96
|
-
self.rejects_last_sample += 1
|
|
97
|
-
interarrival_time += self.arr_rng.exponential(1 / self.lambda_max)
|
|
98
|
-
u = self.thinning_rng.uniform(0.0, 1.0)
|
|
99
|
-
|
|
100
|
-
return interarrival_time
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|