prosimos 2.0.4__tar.gz → 2.0.5__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.
- {prosimos-2.0.4 → prosimos-2.0.5}/PKG-INFO +1 -1
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/all_attributes.py +1 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/branch_condition_parser.py +1 -5
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/branch_condition_rules.py +26 -5
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/case_attributes.py +6 -3
- prosimos-2.0.5/prosimos/event_attributes.py +245 -0
- prosimos-2.0.5/prosimos/gateway_condition_choice.py +15 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/outgoing_flow_selector.py +57 -25
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_engine.py +2 -8
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_properties_parser.py +26 -3
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_setup.py +2 -2
- {prosimos-2.0.4 → prosimos-2.0.5}/pyproject.toml +1 -1
- prosimos-2.0.4/prosimos/event_attributes.py +0 -124
- prosimos-2.0.4/prosimos/gateway_condition_choice.py +0 -10
- {prosimos-2.0.4 → prosimos-2.0.5}/README.md +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/__init__.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/emd_metric.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/exceptions.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/inter_arrival_cases_discovery.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/log_comparison_metrics.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/log_parser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/cli/__init__.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/cli/diff_res_bpsim.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/__init__.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/batch_processing.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/batch_processing_parser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/control_flow_manager.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/event_attributes_parser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/exceptions.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/execution_info.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/file_manager.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/fuzzy_engine/event_log_analyser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/fuzzy_engine/fuzzy_calendar.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/global_attributes.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/global_attributes_parser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/histogram_distribution.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation_parser.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation_rules.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/probability_distributions.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/resource_profile.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_execution_stats.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_queues_ds.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_stats.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_stats_calculator.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/warning_logger.py +0 -0
- {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/weekday_helper.py +0 -0
|
@@ -15,6 +15,7 @@ class AllAttributes:
|
|
|
15
15
|
global_case_attribute_names = global_attribute_names.intersection(all_case_attribute_names)
|
|
16
16
|
global_event_attribute_names = global_attribute_names.intersection(all_event_attribute_names)
|
|
17
17
|
|
|
18
|
+
self.global_attribute_initial_values = global_attributes.get_values_calculated()
|
|
18
19
|
self.global_attributes = self._extract_global_attributes(global_attributes, global_case_attribute_names, global_event_attribute_names)
|
|
19
20
|
|
|
20
21
|
self.global_case_attributes, self.case_attributes = \
|
|
@@ -7,6 +7,7 @@ from prosimos.branch_condition_rules import (
|
|
|
7
7
|
BranchConditionWithRule
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class BranchConditionParser:
|
|
11
12
|
def __init__(self, json_data_with_branching_conditions):
|
|
12
13
|
self.data = json_data_with_branching_conditions
|
|
@@ -17,25 +18,20 @@ class BranchConditionParser:
|
|
|
17
18
|
or_rules_json = curr_branch_condition_rule["rules"]
|
|
18
19
|
branch_id = curr_branch_condition_rule["id"]
|
|
19
20
|
|
|
20
|
-
# print(f"OR RULES JSON: {str(or_rules_json)}")
|
|
21
21
|
or_rules: List[AndBranchConditionRule] = []
|
|
22
22
|
for or_rule in or_rules_json:
|
|
23
23
|
and_rules: List[BranchConditionRule] = []
|
|
24
|
-
# print(f"OR RULE: {str(or_rule)}")
|
|
25
24
|
for and_rule_json in or_rule:
|
|
26
|
-
# print(f"AND RULE JSON: {str(and_rule_json)}")
|
|
27
25
|
rule: BranchConditionRule = BranchConditionRule(
|
|
28
26
|
and_rule_json["attribute"],
|
|
29
27
|
and_rule_json["comparison"],
|
|
30
28
|
and_rule_json["value"],
|
|
31
29
|
)
|
|
32
30
|
and_rules.append(rule)
|
|
33
|
-
# print(f"AND RULES: {str(or_rule)}")
|
|
34
31
|
all_and_rules = AndBranchConditionRule(and_rules)
|
|
35
32
|
or_rules.append(all_and_rules)
|
|
36
33
|
|
|
37
34
|
all_or_rules = OrBranchConditionRule(or_rules)
|
|
38
|
-
# print(f"ALL OR RULES: {all_or_rules}\n\n")
|
|
39
35
|
|
|
40
36
|
branching_conditions.append(BranchConditionWithRule(all_or_rules, branch_id))
|
|
41
37
|
|
|
@@ -26,7 +26,6 @@ class BranchConditionRule:
|
|
|
26
26
|
if self.condition == "in":
|
|
27
27
|
min_boundary = float(value[0])
|
|
28
28
|
max_boundary = sys.maxsize if value[1] == "inf" else float(value[1])
|
|
29
|
-
|
|
30
29
|
return min_boundary, max_boundary
|
|
31
30
|
else:
|
|
32
31
|
return value
|
|
@@ -35,12 +34,34 @@ class BranchConditionRule:
|
|
|
35
34
|
if self.attribute not in all_case_values:
|
|
36
35
|
return False
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
raw_case_value = all_case_values[self.attribute]
|
|
38
|
+
if self.condition in [">", ">=", "<", "<=", "!="]:
|
|
39
|
+
try:
|
|
40
|
+
case_value = float(raw_case_value)
|
|
41
|
+
comparison_value = float(self.value)
|
|
42
|
+
except ValueError:
|
|
43
|
+
case_value = raw_case_value
|
|
44
|
+
comparison_value = self.value
|
|
45
|
+
else:
|
|
46
|
+
case_value = raw_case_value
|
|
47
|
+
comparison_value = self.value
|
|
48
|
+
|
|
39
49
|
if self.condition == "in":
|
|
40
|
-
|
|
41
|
-
return
|
|
50
|
+
min_value, max_value = comparison_value
|
|
51
|
+
return min_value <= case_value <= max_value
|
|
52
|
+
elif self.condition == ">":
|
|
53
|
+
return case_value > comparison_value
|
|
54
|
+
elif self.condition == ">=":
|
|
55
|
+
return case_value >= comparison_value
|
|
56
|
+
elif self.condition == "<":
|
|
57
|
+
return case_value < comparison_value
|
|
58
|
+
elif self.condition == "<=":
|
|
59
|
+
return case_value <= comparison_value
|
|
60
|
+
elif self.condition == "!=":
|
|
61
|
+
return case_value != comparison_value
|
|
42
62
|
else:
|
|
43
|
-
return
|
|
63
|
+
return case_value == comparison_value
|
|
64
|
+
|
|
44
65
|
|
|
45
66
|
|
|
46
67
|
class AndBranchConditionRule:
|
|
@@ -52,12 +52,15 @@ class CaseAttribute:
|
|
|
52
52
|
return self.value.generate_sample(1)[0]
|
|
53
53
|
|
|
54
54
|
def validate(self):
|
|
55
|
+
epsilon = 1e-6
|
|
56
|
+
|
|
55
57
|
if self.case_atrr_type == CASE_ATTR_TYPE.DISCRETE:
|
|
56
58
|
actual_sum_probabilities = reduce(lambda acc, item: acc + item, self.value["probabilities"], 0)
|
|
57
59
|
|
|
58
|
-
if actual_sum_probabilities
|
|
59
|
-
raise InvalidCaseAttributeException(
|
|
60
|
-
|
|
60
|
+
if not (1 - epsilon <= actual_sum_probabilities <= 1 + epsilon):
|
|
61
|
+
raise InvalidCaseAttributeException(
|
|
62
|
+
f"Case attribute {self.name}: probabilities' sum should be equal to 1")
|
|
63
|
+
|
|
61
64
|
return True
|
|
62
65
|
|
|
63
66
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import pprint
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from random import choices
|
|
4
|
+
from typing import Dict
|
|
5
|
+
import ast
|
|
6
|
+
import operator as op
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from pix_framework.statistics.distribution import DurationDistribution
|
|
10
|
+
|
|
11
|
+
from prosimos.exceptions import InvalidEventAttributeException
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EVENT_ATTR_TYPE(Enum):
|
|
16
|
+
DISCRETE = "discrete"
|
|
17
|
+
CONTINUOUS = "continuous"
|
|
18
|
+
EXPRESSION = "expression"
|
|
19
|
+
DTREE = "dtree"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
operators = {
|
|
23
|
+
ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv,
|
|
24
|
+
ast.USub: op.neg, ast.Eq: op.eq, ast.NotEq: op.ne, ast.Lt: op.lt,
|
|
25
|
+
ast.LtE: op.le, ast.Gt: op.gt, ast.GtE: op.ge, ast.Not: op.not_,
|
|
26
|
+
ast.And: op.and_, ast.Or: op.or_, ast.Mod: op.mod, ast.Pow: op.pow,
|
|
27
|
+
ast.FloorDiv: op.floordiv
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def fix(mean):
|
|
31
|
+
return DurationDistribution(name="fix", mean=mean, var=0.0, std=0.0, minimum=mean, maximum=mean).generate_sample(1)[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def uniform(minimum, maximum):
|
|
35
|
+
return DurationDistribution(name="uniform", minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def norm(mean, std, minimum=None, maximum=None):
|
|
39
|
+
return DurationDistribution(name="norm", mean=mean, std=std, minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def triang(c, minimum, maximum):
|
|
43
|
+
return DurationDistribution(name="triang", mean=c, minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def expon(mean, minimum=None, maximum=None):
|
|
47
|
+
return DurationDistribution(name="expon", mean=mean, minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lognorm(mean, var, minimum=None, maximum=None):
|
|
51
|
+
return DurationDistribution(name="lognorm", mean=mean, var=var, minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def gamma(mean, var, minimum=None, maximum=None):
|
|
55
|
+
return DurationDistribution(name="gamma", mean=mean, var=var, minimum=minimum, maximum=maximum).generate_sample(1)[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
distributions = {
|
|
59
|
+
'fix': fix,
|
|
60
|
+
'uniform': uniform,
|
|
61
|
+
'norm': norm,
|
|
62
|
+
'triang': triang,
|
|
63
|
+
'expon': expon,
|
|
64
|
+
'lognorm': lognorm,
|
|
65
|
+
'gamma': gamma
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
math_functions = {name: getattr(math, name) for name in dir(math) if callable(getattr(math, name))}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_discrete_value(value_info):
|
|
72
|
+
if isinstance(value_info, list):
|
|
73
|
+
prob_arr = []
|
|
74
|
+
options_arr = []
|
|
75
|
+
for item in value_info:
|
|
76
|
+
options_arr.append(item["key"])
|
|
77
|
+
prob_arr.append(float(item["value"]))
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"type": "discrete",
|
|
81
|
+
"options": options_arr,
|
|
82
|
+
"probabilities": prob_arr
|
|
83
|
+
}
|
|
84
|
+
elif isinstance(value_info, dict):
|
|
85
|
+
return {
|
|
86
|
+
"type": "markov",
|
|
87
|
+
"transitions": value_info
|
|
88
|
+
}
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("Unsupported value_info format for discrete value")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_continuous_value(value_info) -> "DurationDistribution":
|
|
94
|
+
return DurationDistribution.from_dict(value_info)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def eval_expr(expr, vars_dict):
|
|
98
|
+
try:
|
|
99
|
+
tree = ast.parse(expr, mode='eval')
|
|
100
|
+
except (SyntaxError, ZeroDivisionError, TypeError, KeyError):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _eval(node):
|
|
104
|
+
try:
|
|
105
|
+
if isinstance(node, ast.Num):
|
|
106
|
+
return node.n
|
|
107
|
+
elif isinstance(node, ast.BinOp):
|
|
108
|
+
return operators[type(node.op)](_eval(node.left), _eval(node.right))
|
|
109
|
+
elif isinstance(node, ast.UnaryOp):
|
|
110
|
+
return operators[type(node.op)](_eval(node.operand))
|
|
111
|
+
elif isinstance(node, ast.Name):
|
|
112
|
+
if node.id in vars_dict:
|
|
113
|
+
return vars_dict[node.id]
|
|
114
|
+
elif isinstance(node, ast.Str):
|
|
115
|
+
return node.s
|
|
116
|
+
elif isinstance(node, ast.Compare):
|
|
117
|
+
return operators[type(node.ops[0])](_eval(node.left), _eval(node.comparators[0]))
|
|
118
|
+
elif isinstance(node, ast.BoolOp):
|
|
119
|
+
if type(node.op) is ast.And:
|
|
120
|
+
return all(_eval(value) for value in node.values)
|
|
121
|
+
elif type(node.op) is ast.Or:
|
|
122
|
+
return any(_eval(value) for value in node.values)
|
|
123
|
+
elif isinstance(node, ast.Call):
|
|
124
|
+
func_name = node.func.id
|
|
125
|
+
args = [_eval(arg) for arg in node.args]
|
|
126
|
+
if func_name in distributions:
|
|
127
|
+
return distributions[func_name](*args)
|
|
128
|
+
elif func_name in math_functions:
|
|
129
|
+
args = [_eval(arg) for arg in node.args]
|
|
130
|
+
try:
|
|
131
|
+
return math_functions[func_name ](*args)
|
|
132
|
+
except OverflowError:
|
|
133
|
+
return np.finfo(np.float32).max
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
return 0
|
|
136
|
+
else:
|
|
137
|
+
return 0
|
|
138
|
+
except (SyntaxError, ZeroDivisionError, TypeError, KeyError):
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
return _eval(tree.body)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def evaluate_dtree(dtree, vars_dict):
|
|
145
|
+
for conditions, formula in dtree:
|
|
146
|
+
if conditions is True or all(eval_expr(cond, vars_dict) for cond in conditions):
|
|
147
|
+
return eval_expr(formula, vars_dict)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
class EventAttribute:
|
|
151
|
+
def __init__(self, event_id, name, event_attr_type, value):
|
|
152
|
+
self.event_id: str = event_id
|
|
153
|
+
self.name: str = name
|
|
154
|
+
self.event_attr_type: EVENT_ATTR_TYPE = EVENT_ATTR_TYPE(event_attr_type)
|
|
155
|
+
|
|
156
|
+
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
157
|
+
self.value = parse_discrete_value(value)
|
|
158
|
+
elif self.event_attr_type == EVENT_ATTR_TYPE.CONTINUOUS:
|
|
159
|
+
self.value = parse_continuous_value(value)
|
|
160
|
+
elif self.event_attr_type == EVENT_ATTR_TYPE.EXPRESSION:
|
|
161
|
+
self.value = value
|
|
162
|
+
elif self.event_attr_type == EVENT_ATTR_TYPE.DTREE:
|
|
163
|
+
self.value = value
|
|
164
|
+
else:
|
|
165
|
+
raise Exception(f"Not supported event attribute {type}")
|
|
166
|
+
|
|
167
|
+
self.validate()
|
|
168
|
+
|
|
169
|
+
def get_next_value(self, all_attributes):
|
|
170
|
+
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
171
|
+
if self.value["type"] == "markov":
|
|
172
|
+
current_value = all_attributes.get(self.name, None)
|
|
173
|
+
next_state = self.get_next_markov_state(current_value)
|
|
174
|
+
if next_state is not None:
|
|
175
|
+
all_attributes[self.name] = next_state
|
|
176
|
+
return next_state
|
|
177
|
+
return current_value
|
|
178
|
+
else:
|
|
179
|
+
one_choice_arr = choices(self.value["options"], self.value["probabilities"])
|
|
180
|
+
return one_choice_arr[0]
|
|
181
|
+
|
|
182
|
+
elif self.event_attr_type == EVENT_ATTR_TYPE.EXPRESSION:
|
|
183
|
+
result = eval_expr(self.value, all_attributes)
|
|
184
|
+
if isinstance(result, (int, float, np.number)) and not isinstance(result, bool):
|
|
185
|
+
if result == 0: # Specifically handle zero without adjusting to tiny (in case of any errors in eval)
|
|
186
|
+
return 0
|
|
187
|
+
elif result == float('inf'):
|
|
188
|
+
return np.finfo(np.float32).max
|
|
189
|
+
elif result == -float('inf'):
|
|
190
|
+
return np.finfo(np.float32).min
|
|
191
|
+
elif abs(result) < np.finfo(np.float32).tiny:
|
|
192
|
+
return np.finfo(np.float32).tiny
|
|
193
|
+
elif abs(result) > np.finfo(np.float32).max:
|
|
194
|
+
return np.finfo(np.float32).max if result > 0 else np.finfo(np.float32).min
|
|
195
|
+
else:
|
|
196
|
+
return result
|
|
197
|
+
else:
|
|
198
|
+
return result
|
|
199
|
+
elif self.event_attr_type == EVENT_ATTR_TYPE.DTREE:
|
|
200
|
+
result = evaluate_dtree(self.value, all_attributes)
|
|
201
|
+
if result is not None:
|
|
202
|
+
return result
|
|
203
|
+
return 0
|
|
204
|
+
else:
|
|
205
|
+
return self.value.generate_sample(1)[0]
|
|
206
|
+
|
|
207
|
+
def get_next_markov_state(self, current_value):
|
|
208
|
+
transitions = self.value["transitions"]
|
|
209
|
+
if current_value in transitions:
|
|
210
|
+
current_transitions = transitions[current_value]
|
|
211
|
+
options, probabilities = zip(*current_transitions.items())
|
|
212
|
+
return choices(options, probabilities)[0]
|
|
213
|
+
return current_value
|
|
214
|
+
|
|
215
|
+
def validate(self):
|
|
216
|
+
epsilon = 1e-6
|
|
217
|
+
|
|
218
|
+
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
219
|
+
if self.value["type"] == "discrete":
|
|
220
|
+
actual_sum_probabilities = sum(self.value["probabilities"])
|
|
221
|
+
|
|
222
|
+
if not (1 - epsilon <= actual_sum_probabilities <= 1 + epsilon):
|
|
223
|
+
raise InvalidEventAttributeException(
|
|
224
|
+
f"Event attribute {self.name}: probabilities' sum should be equal to 1")
|
|
225
|
+
elif self.value["type"] == "markov":
|
|
226
|
+
for state, transitions in self.value["transitions"].items():
|
|
227
|
+
actual_sum_probabilities = sum(transitions.values())
|
|
228
|
+
if not (1 - epsilon <= actual_sum_probabilities <= 1 + epsilon):
|
|
229
|
+
raise InvalidEventAttributeException(
|
|
230
|
+
f"Event attribute {self.name}, state {state}: "
|
|
231
|
+
f"probabilities' sum should be equal to 1")
|
|
232
|
+
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class AllEventAttributes:
|
|
237
|
+
def __init__(self, event_attr_arr: Dict[str, Dict[str, EventAttribute]]):
|
|
238
|
+
self.attributes = event_attr_arr
|
|
239
|
+
|
|
240
|
+
def get_columns_generated(self):
|
|
241
|
+
return list({attr.name for event_id in self.attributes for attr in self.attributes[event_id].values()})
|
|
242
|
+
|
|
243
|
+
def get_values_calculated(self):
|
|
244
|
+
return {attr.name: attr.get_next_value() for event_id in self.attributes for attr in
|
|
245
|
+
self.attributes[event_id].values()}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class GatewayConditionChoice:
|
|
2
|
+
def __init__(self, candidates_list, rules_list, default_path=None):
|
|
3
|
+
self.candidates_list = candidates_list
|
|
4
|
+
self.rules_list = rules_list
|
|
5
|
+
self.default_path = default_path
|
|
6
|
+
|
|
7
|
+
def set_default(self, default_path):
|
|
8
|
+
self.default_path = default_path
|
|
9
|
+
|
|
10
|
+
def get_outgoing_flow(self, attributes):
|
|
11
|
+
return [candidate for candidate, rule in zip(self.candidates_list, self.rules_list)
|
|
12
|
+
if rule.is_true(attributes)]
|
|
13
|
+
|
|
14
|
+
def get_default_path(self):
|
|
15
|
+
return self.default_path
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import pprint
|
|
3
|
+
|
|
2
4
|
from prosimos.control_flow_manager import BPMN
|
|
3
5
|
from prosimos.probability_distributions import Choice
|
|
4
6
|
from prosimos.gateway_condition_choice import GatewayConditionChoice
|
|
@@ -19,30 +21,39 @@ class OutgoingFlowSelector:
|
|
|
19
21
|
|
|
20
22
|
@staticmethod
|
|
21
23
|
def _handle_exclusive_gateway(e_info, element_probability, all_attributes, gateway_conditions):
|
|
22
|
-
curr_gateway_conditions = gateway_conditions
|
|
23
|
-
candidates_list = curr_gateway_conditions.candidates_list
|
|
24
|
+
curr_gateway_conditions = gateway_conditions.get(e_info.id, None)
|
|
24
25
|
|
|
25
26
|
# No conditions (use probabilities)
|
|
26
|
-
if not candidates_list:
|
|
27
|
+
if not curr_gateway_conditions or not curr_gateway_conditions.candidates_list:
|
|
27
28
|
return OutgoingFlowSelector._use_probabilities(e_info, element_probability)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
passed_arcs_ids =
|
|
29
|
+
|
|
30
|
+
candidates_list = curr_gateway_conditions.candidates_list
|
|
31
|
+
passed_arcs_ids = []
|
|
32
|
+
|
|
33
|
+
# Evaluate conditions
|
|
34
|
+
for candidate in candidates_list:
|
|
35
|
+
condition = curr_gateway_conditions.rules_list[candidates_list.index(candidate)]
|
|
36
|
+
if not condition or condition.is_true(all_attributes):
|
|
37
|
+
passed_arcs_ids.append(candidate)
|
|
38
|
+
|
|
39
|
+
# All conditions evaluated to false
|
|
40
|
+
if not passed_arcs_ids:
|
|
41
|
+
default_path = curr_gateway_conditions.get_default_path()
|
|
42
|
+
if default_path:
|
|
43
|
+
warning_logger.add_warning(f"[XOR] {e_info.id} all conditions evaluated to false. Using default path {default_path}.")
|
|
44
|
+
return [(default_path, None)]
|
|
45
|
+
else:
|
|
46
|
+
warning_logger.add_warning(f"[XOR] {e_info.id} all conditions evaluated to false and no default path. Using probabilities.")
|
|
47
|
+
return OutgoingFlowSelector._use_probabilities(e_info, element_probability)
|
|
31
48
|
|
|
32
49
|
# One true condition
|
|
33
50
|
if len(passed_arcs_ids) == 1:
|
|
34
51
|
return [(passed_arcs_ids[0], None)]
|
|
35
52
|
|
|
36
|
-
# All true or all false
|
|
37
|
-
if len(passed_arcs_ids) in [0, len(candidates_list)]:
|
|
38
|
-
warning_logger.add_warning(f"{e_info.id} all XOR gateway conditions evaluated to the same result ")
|
|
39
|
-
return OutgoingFlowSelector._use_probabilities(e_info, element_probability)
|
|
40
|
-
|
|
41
53
|
# More than 1 true (use scaled probabilities)
|
|
42
54
|
curr_candidates = element_probability[e_info.id].candidates_list
|
|
43
55
|
curr_probabilities = element_probability[e_info.id].probability_list
|
|
44
|
-
passed_arcs_probs = OutgoingFlowSelector._get_probabilities(passed_arcs_ids, curr_candidates,
|
|
45
|
-
curr_probabilities)
|
|
56
|
+
passed_arcs_probs = OutgoingFlowSelector._get_probabilities(passed_arcs_ids, curr_candidates, curr_probabilities)
|
|
46
57
|
|
|
47
58
|
scaled_probabilities = OutgoingFlowSelector._scale_to_one(passed_arcs_probs)
|
|
48
59
|
choice = Choice(passed_arcs_ids, scaled_probabilities)
|
|
@@ -50,25 +61,46 @@ class OutgoingFlowSelector:
|
|
|
50
61
|
warning_logger.add_warning(f"{e_info.id} more than 1 XOR gateway conditions evaluated to positive result. Scaled probabilities were used.")
|
|
51
62
|
return [(choice.get_outgoing_flow(), None)]
|
|
52
63
|
|
|
53
|
-
|
|
54
64
|
@staticmethod
|
|
55
65
|
def _handle_inclusive_gateway(e_info, element_probability, all_attributes, gateway_conditions):
|
|
56
|
-
curr_gateway_conditions = gateway_conditions
|
|
57
|
-
candidates_list = curr_gateway_conditions.candidates_list
|
|
66
|
+
curr_gateway_conditions = gateway_conditions.get(e_info.id, None)
|
|
58
67
|
|
|
59
68
|
# No conditions (use probabilities)
|
|
60
|
-
if not candidates_list:
|
|
69
|
+
if not curr_gateway_conditions or not curr_gateway_conditions.candidates_list:
|
|
61
70
|
return element_probability[e_info.id].get_multiple_flows()
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
passed_arcs_ids =
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
candidates_list = curr_gateway_conditions.candidates_list
|
|
73
|
+
passed_arcs_ids = []
|
|
74
|
+
evaluated_arcs_ids = []
|
|
75
|
+
|
|
76
|
+
# Evaluate conditions
|
|
77
|
+
for candidate in candidates_list:
|
|
78
|
+
condition = curr_gateway_conditions.rules_list[candidates_list.index(candidate)]
|
|
79
|
+
if condition:
|
|
80
|
+
evaluated_arcs_ids.append(candidate)
|
|
81
|
+
if condition.is_true(all_attributes):
|
|
82
|
+
passed_arcs_ids.append(candidate)
|
|
83
|
+
|
|
84
|
+
# All conditions evaluated to false
|
|
67
85
|
if not passed_arcs_ids:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
default_path = curr_gateway_conditions.get_default_path()
|
|
87
|
+
if default_path:
|
|
88
|
+
warning_logger.add_warning(f"[OR] {e_info.id} all conditions evaluated to false. Using default path {default_path}.")
|
|
89
|
+
return [(default_path, None)]
|
|
90
|
+
else:
|
|
91
|
+
warning_logger.add_warning(f"[OR] {e_info.id} all conditions evaluated to false and no default path. Using probabilities.")
|
|
92
|
+
return element_probability[e_info.id].get_multiple_flows()
|
|
93
|
+
|
|
94
|
+
# Execute true conditions
|
|
95
|
+
result_flows = [(flow, None) for flow in passed_arcs_ids]
|
|
96
|
+
|
|
97
|
+
# Use probabilities for the rest of the flows where conditions are missing
|
|
98
|
+
missing_conditions_flows = [flow for flow in candidates_list if flow not in evaluated_arcs_ids]
|
|
99
|
+
if missing_conditions_flows:
|
|
100
|
+
probabilities = element_probability[e_info.id].get_multiple_flows()
|
|
101
|
+
result_flows.extend([(flow, None) for flow in probabilities if flow in missing_conditions_flows])
|
|
102
|
+
|
|
103
|
+
return result_flows
|
|
72
104
|
|
|
73
105
|
@staticmethod
|
|
74
106
|
def _handle_parallel_events(e_info):
|
|
@@ -35,7 +35,6 @@ class SimResource:
|
|
|
35
35
|
|
|
36
36
|
class SimBPMEnv:
|
|
37
37
|
def __init__(self, sim_setup: SimDiffSetup, stat_fwriter, log_fwriter):
|
|
38
|
-
self.produced_event_attributes = {}
|
|
39
38
|
self.sim_setup = sim_setup
|
|
40
39
|
self.sim_resources = dict()
|
|
41
40
|
self.stat_fwriter = stat_fwriter
|
|
@@ -61,7 +60,7 @@ class SimBPMEnv:
|
|
|
61
60
|
)
|
|
62
61
|
|
|
63
62
|
all_attributes = {
|
|
64
|
-
"global": self.sim_setup.all_attributes.
|
|
63
|
+
"global": self.sim_setup.all_attributes.global_attribute_initial_values,
|
|
65
64
|
**self.case_prioritisation.all_case_attributes
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -259,7 +258,6 @@ class SimBPMEnv:
|
|
|
259
258
|
|
|
260
259
|
new_global_attr_values = self._extract_attributes_for_event(current_event.task_id, global_event_attributes, all_attribute_values)
|
|
261
260
|
new_event_attr_values = self._extract_attributes_for_event(current_event.task_id, event_attributes, all_attribute_values)
|
|
262
|
-
self.produced_event_attributes = set(new_event_attr_values.keys())
|
|
263
261
|
|
|
264
262
|
self.sim_setup.bpmn_graph.all_attributes["global"].update(new_global_attr_values)
|
|
265
263
|
self.sim_setup.bpmn_graph.all_attributes[current_event.p_case].update(new_event_attr_values)
|
|
@@ -298,11 +296,7 @@ class SimBPMEnv:
|
|
|
298
296
|
)
|
|
299
297
|
|
|
300
298
|
all_attrs = self.sim_setup.bpmn_graph.get_all_attributes(full_event.p_case)
|
|
301
|
-
values = [all_attrs.get(col)
|
|
302
|
-
(col in self.produced_event_attributes or col not in self.sim_setup.all_attributes.event_attribute_names)
|
|
303
|
-
else None for col in self.additional_columns]
|
|
304
|
-
|
|
305
|
-
self.produced_event_attributes.clear()
|
|
299
|
+
values = ["" if all_attrs.get(col) is None else all_attrs.get(col) for col in self.additional_columns]
|
|
306
300
|
|
|
307
301
|
return [*row_basic_info, *values]
|
|
308
302
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import json
|
|
3
|
+
import pprint
|
|
3
4
|
import xml.etree.ElementTree as ET
|
|
4
5
|
|
|
5
6
|
from dateutil import parser
|
|
@@ -288,17 +289,21 @@ def parse_gateway_conditions(gateway_json, branch_rules):
|
|
|
288
289
|
missing_conditions = list()
|
|
289
290
|
|
|
290
291
|
for prob_info in g_info["probabilities"]:
|
|
292
|
+
path_id = prob_info["path_id"]
|
|
293
|
+
|
|
291
294
|
if "condition_id" in prob_info:
|
|
292
|
-
out_arc.append(
|
|
295
|
+
out_arc.append(path_id)
|
|
293
296
|
curr_branch_rules = branch_rules.get_branch_condition_by_id(prob_info["condition_id"])
|
|
294
297
|
gateway_rules.append(curr_branch_rules)
|
|
295
298
|
else:
|
|
296
|
-
missing_conditions.append(
|
|
299
|
+
missing_conditions.append(path_id)
|
|
297
300
|
|
|
298
301
|
if len(prob_info) > 0:
|
|
299
302
|
gateway_conditions[g_id] = GatewayConditionChoice(out_arc, gateway_rules)
|
|
300
303
|
|
|
301
|
-
if len(missing_conditions) > 0 and
|
|
304
|
+
if len(missing_conditions) > 0 and \
|
|
305
|
+
len(missing_conditions) != g_info["probabilities"] and \
|
|
306
|
+
1 < len(g_info["probabilities"]) != len(gateway_rules):
|
|
302
307
|
warning_logger.add_warning(f"Gateway {g_id} is using conditions, but some are missing. Flows without conditions: {', '.join(missing_conditions)}")
|
|
303
308
|
|
|
304
309
|
return gateway_conditions
|
|
@@ -676,3 +681,21 @@ def parse_datetime(time, has_date):
|
|
|
676
681
|
except ValueError:
|
|
677
682
|
pass
|
|
678
683
|
raise ValueError
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def add_default_flows(gateway_conditions, bpmn_path):
|
|
687
|
+
tree = ET.parse(bpmn_path)
|
|
688
|
+
root = tree.getroot()
|
|
689
|
+
|
|
690
|
+
namespaces = {'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL'}
|
|
691
|
+
|
|
692
|
+
gateway_types = ['exclusiveGateway', 'inclusiveGateway']
|
|
693
|
+
for gateway_type in gateway_types:
|
|
694
|
+
for gateway in root.findall(f'.//bpmn:{gateway_type}', namespaces):
|
|
695
|
+
gateway_id = gateway.get('id')
|
|
696
|
+
default_flow = gateway.get('default')
|
|
697
|
+
|
|
698
|
+
if gateway_id in gateway_conditions and default_flow:
|
|
699
|
+
gateway_conditions[gateway_id].set_default(default_flow)
|
|
700
|
+
|
|
701
|
+
return gateway_conditions
|
|
@@ -11,7 +11,7 @@ from prosimos.batch_processing import BatchConfigPerTask
|
|
|
11
11
|
from prosimos.control_flow_manager import BPMN, ElementInfo, ProcessState
|
|
12
12
|
from prosimos.exceptions import InvalidSimScenarioException
|
|
13
13
|
from prosimos.histogram_distribution import HistogramDistribution
|
|
14
|
-
from prosimos.simulation_properties_parser import parse_json_sim_parameters, parse_simulation_model
|
|
14
|
+
from prosimos.simulation_properties_parser import parse_json_sim_parameters, parse_simulation_model, add_default_flows
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class SimDiffSetup:
|
|
@@ -34,7 +34,7 @@ class SimDiffSetup:
|
|
|
34
34
|
self.gateway_execution_limit,
|
|
35
35
|
self.model_type
|
|
36
36
|
) = parse_json_sim_parameters(json_path)
|
|
37
|
-
|
|
37
|
+
self.gateway_conditions = add_default_flows(self.gateway_conditions, bpmn_path)
|
|
38
38
|
self.case_attributes = self.all_attributes.case_attributes
|
|
39
39
|
|
|
40
40
|
self.bpmn_graph = parse_simulation_model(bpmn_path)
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from functools import reduce
|
|
3
|
-
from random import choices
|
|
4
|
-
from typing import Dict
|
|
5
|
-
import ast
|
|
6
|
-
import operator as op
|
|
7
|
-
|
|
8
|
-
from pix_framework.statistics.distribution import DurationDistribution
|
|
9
|
-
|
|
10
|
-
from prosimos.exceptions import InvalidEventAttributeException
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class EVENT_ATTR_TYPE(Enum):
|
|
14
|
-
DISCRETE = "discrete"
|
|
15
|
-
CONTINUOUS = "continuous"
|
|
16
|
-
EXPRESSION = "expression"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
operators = {
|
|
20
|
-
ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv,
|
|
21
|
-
ast.USub: op.neg, ast.Eq: op.eq, ast.NotEq: op.ne, ast.Lt: op.lt,
|
|
22
|
-
ast.LtE: op.le, ast.Gt: op.gt, ast.GtE: op.ge, ast.Not: op.not_,
|
|
23
|
-
ast.And: op.and_, ast.Or: op.or_, ast.Mod: op.mod, ast.Pow: op.pow,
|
|
24
|
-
ast.FloorDiv: op.floordiv
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def parse_discrete_value(value_info_arr):
|
|
29
|
-
prob_arr = []
|
|
30
|
-
options_arr = []
|
|
31
|
-
for item in value_info_arr:
|
|
32
|
-
options_arr.append(item["key"])
|
|
33
|
-
prob_arr.append(float(item["value"]))
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
"options": options_arr,
|
|
37
|
-
"probabilities": prob_arr
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def parse_continuous_value(value_info) -> "DurationDistribution":
|
|
42
|
-
return DurationDistribution.from_dict(value_info)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def eval_expr(expr, vars_dict):
|
|
46
|
-
try:
|
|
47
|
-
tree = ast.parse(expr, mode='eval')
|
|
48
|
-
except (SyntaxError, ZeroDivisionError, TypeError, KeyError):
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
def _eval(node):
|
|
52
|
-
try:
|
|
53
|
-
if isinstance(node, ast.Num):
|
|
54
|
-
return node.n
|
|
55
|
-
elif isinstance(node, ast.BinOp):
|
|
56
|
-
return operators[type(node.op)](_eval(node.left), _eval(node.right))
|
|
57
|
-
elif isinstance(node, ast.UnaryOp):
|
|
58
|
-
return operators[type(node.op)](_eval(node.operand))
|
|
59
|
-
elif isinstance(node, ast.Name):
|
|
60
|
-
return vars_dict[node.id]
|
|
61
|
-
elif isinstance(node, ast.Str):
|
|
62
|
-
return node.s
|
|
63
|
-
elif isinstance(node, ast.Compare):
|
|
64
|
-
return operators[type(node.ops[0])](_eval(node.left), _eval(node.comparators[0]))
|
|
65
|
-
elif isinstance(node, ast.BoolOp):
|
|
66
|
-
if type(node.op) is ast.And:
|
|
67
|
-
return all(_eval(value) for value in node.values)
|
|
68
|
-
elif type(node.op) is ast.Or:
|
|
69
|
-
return any(_eval(value) for value in node.values)
|
|
70
|
-
else:
|
|
71
|
-
return None
|
|
72
|
-
except (SyntaxError, ZeroDivisionError, TypeError, KeyError):
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
return _eval(tree.body)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class EventAttribute:
|
|
79
|
-
def __init__(self, event_id, name, event_attr_type, value):
|
|
80
|
-
self.event_id: str = event_id
|
|
81
|
-
self.name: str = name
|
|
82
|
-
self.event_attr_type: EVENT_ATTR_TYPE = EVENT_ATTR_TYPE(event_attr_type)
|
|
83
|
-
|
|
84
|
-
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
85
|
-
self.value = parse_discrete_value(value)
|
|
86
|
-
elif self.event_attr_type == EVENT_ATTR_TYPE.CONTINUOUS:
|
|
87
|
-
self.value = parse_continuous_value(value)
|
|
88
|
-
elif self.event_attr_type == EVENT_ATTR_TYPE.EXPRESSION:
|
|
89
|
-
self.value = value
|
|
90
|
-
else:
|
|
91
|
-
raise Exception(f"Not supported event attribute {type}")
|
|
92
|
-
|
|
93
|
-
self.validate()
|
|
94
|
-
|
|
95
|
-
def get_next_value(self, all_attributes={}):
|
|
96
|
-
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
97
|
-
one_choice_arr = choices(self.value["options"], self.value["probabilities"])
|
|
98
|
-
return one_choice_arr[0]
|
|
99
|
-
elif self.event_attr_type == EVENT_ATTR_TYPE.EXPRESSION:
|
|
100
|
-
return eval_expr(self.value, all_attributes)
|
|
101
|
-
else:
|
|
102
|
-
return self.value.generate_sample(1)[0]
|
|
103
|
-
|
|
104
|
-
def validate(self):
|
|
105
|
-
if self.event_attr_type == EVENT_ATTR_TYPE.DISCRETE:
|
|
106
|
-
actual_sum_probabilities = reduce(lambda acc, item: acc + item, self.value["probabilities"], 0)
|
|
107
|
-
|
|
108
|
-
if actual_sum_probabilities != 1:
|
|
109
|
-
raise InvalidEventAttributeException(
|
|
110
|
-
f"Event attribute ${self.name}: probabilities' sum should be equal to 1")
|
|
111
|
-
|
|
112
|
-
return True
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class AllEventAttributes:
|
|
116
|
-
def __init__(self, event_attr_arr: Dict[str, Dict[str, EventAttribute]]):
|
|
117
|
-
self.attributes = event_attr_arr
|
|
118
|
-
|
|
119
|
-
def get_columns_generated(self):
|
|
120
|
-
return list({attr.name for event_id in self.attributes for attr in self.attributes[event_id].values()})
|
|
121
|
-
|
|
122
|
-
def get_values_calculated(self):
|
|
123
|
-
return {attr.name: attr.get_next_value() for event_id in self.attributes for attr in
|
|
124
|
-
self.attributes[event_id].values()}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
class GatewayConditionChoice:
|
|
2
|
-
def __init__(self, candidates_list, rules_list):
|
|
3
|
-
self.candidates_list = candidates_list
|
|
4
|
-
self.rules_list = rules_list
|
|
5
|
-
|
|
6
|
-
def get_outgoing_flow(self, case_attributes):
|
|
7
|
-
return [candidate for candidate, rule in zip(self.candidates_list, self.rules_list)
|
|
8
|
-
if rule.is_true(case_attributes)]
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|