-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsimulator.py
142 lines (118 loc) · 4.62 KB
/
simulator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""
Monte Carlo simulation is a computational technique used in probability theory and statistics to model
and analyze complex systems or processes that involve uncertainty. It involves using random sampling and
statistical analysis to generate a range of possible outcomes for a given system or process.
References:
- https://realpython.com/python-with-statement/
"""
import sys
import logging
from decimal import Decimal
from typing import List, Optional
import numpy as np
from scipy.stats import truncnorm
logger: logging.RootLogger = logging.getLogger(__name__)
class Simulator:
"""
In supply chain management, storage refers to the physical locations where goods or materials
are stored before they are transported to their final destination. Storage facilities can include
warehouses, distribution centers, and other types of storage facilities, such as refrigerated
or climate-controlled environments.
References:
- https://realpython.com/python-with-statement/
"""
def __init__(self, times: int = 1, title: str = 'Example') -> None:
"""
Simulator constructor.
"""
if not isinstance(times, int) or times < 0:
raise AttributeError("Invalid simulator run times:", times)
if not title or not isinstance(title, str):
raise TypeError('Invalid simulator title:', title)
self.times = times
self.title : str = title
self.results: List[Decimal] = []
def __repr__(self) -> str:
"""
String serializer.
"""
return '<Simulator>'
def normal(self, mean: float, std: float, upper: float, lower: float) -> Decimal:
"""
The truncnorm function creates a truncated normal distribution, which is a normal distribution
that is bounded by a lower and an upper limit.
"""
if mean > upper:
raise ValueError('Mean is too high:', mean)
if mean < lower:
raise ValueError('Mean is too low:', mean)
return Decimal(truncnorm((lower - mean) / std, (upper - mean) / std, loc=mean, scale=std).rvs())
def simulate(self) -> callable:
"""
Decorator to run a piece of code multiple times.
Reference:
- https://stackoverflow.com/questions/4930386/python-is-there-a-way-to-get-the-code-inside-the-with-statement
"""
def decorated(fn: callable):
"""
Decorated function to execute multiple times.
"""
for test_number in range(self.times):
logger.info('Simulator | Test[%s]: Starting...', test_number)
result: Decimal = fn()
if not isinstance(result, Decimal):
raise TypeError('Expecting a Decimal, got:', result)
logger.info('Simulator | Test[%s]: %s', test_number, result)
self.results.append(result)
return decorated
@property
def average(self) -> Decimal:
"""
Calculates the average of the simulation tests.
"""
return sum(self.results) / len(self.results) if self.results else Decimal(0)
def __enter__(self) -> 'Simulator':
"""
Context manager initialization.
"""
return self
def __exit__(self, exc_type: Optional[Exception], exc_value: Optional[Exception], exc_tb: Optional['traceback']):
"""
Context manager finalization.
"""
if exc_type is None:
self.summary()
else:
raise RuntimeError('Failed to run simulator!', exc_value, exc_tb) from exc_type
@property
def maximum(self) -> Decimal:
"""
Calculates the maximum of the simulation tests.
"""
return max(self.results) if self.results else Decimal(0)
@property
def minimum(self) -> Decimal:
"""
Calculates the minimum of the simulation tests.
"""
return min(self.results) if self.results else Decimal(0)
def summary(self) -> None:
"""
Prints the summary of the simulator to STDOUT.
"""
print('-' * 50)
print(self.title, 'Summary:')
print('- Simulated:', self.times, 'times')
print('- Average:', self.average)
print('- Maximum:', self.maximum)
print('- Minimum:', self.minimum)
print('-' * 50)
if __name__ == '__main__':
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
with Simulator(times=10) as simulator:
@simulator.simulate()
def main() -> Decimal:
"""
Sample simulator case.
"""
return simulator.normal(mean=3, std=1, upper=10, lower=1)