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.
Files changed (47) hide show
  1. {prosimos-2.0.4 → prosimos-2.0.5}/PKG-INFO +1 -1
  2. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/all_attributes.py +1 -0
  3. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/branch_condition_parser.py +1 -5
  4. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/branch_condition_rules.py +26 -5
  5. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/case_attributes.py +6 -3
  6. prosimos-2.0.5/prosimos/event_attributes.py +245 -0
  7. prosimos-2.0.5/prosimos/gateway_condition_choice.py +15 -0
  8. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/outgoing_flow_selector.py +57 -25
  9. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_engine.py +2 -8
  10. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_properties_parser.py +26 -3
  11. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_setup.py +2 -2
  12. {prosimos-2.0.4 → prosimos-2.0.5}/pyproject.toml +1 -1
  13. prosimos-2.0.4/prosimos/event_attributes.py +0 -124
  14. prosimos-2.0.4/prosimos/gateway_condition_choice.py +0 -10
  15. {prosimos-2.0.4 → prosimos-2.0.5}/README.md +0 -0
  16. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/__init__.py +0 -0
  17. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/emd_metric.py +0 -0
  18. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/exceptions.py +0 -0
  19. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/inter_arrival_cases_discovery.py +0 -0
  20. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/log_comparison_metrics.py +0 -0
  21. {prosimos-2.0.4 → prosimos-2.0.5}/bpdfr_discovery/log_parser.py +0 -0
  22. {prosimos-2.0.4 → prosimos-2.0.5}/cli/__init__.py +0 -0
  23. {prosimos-2.0.4 → prosimos-2.0.5}/cli/diff_res_bpsim.py +0 -0
  24. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/__init__.py +0 -0
  25. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/batch_processing.py +0 -0
  26. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/batch_processing_parser.py +0 -0
  27. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/control_flow_manager.py +0 -0
  28. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/event_attributes_parser.py +0 -0
  29. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/exceptions.py +0 -0
  30. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/execution_info.py +0 -0
  31. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/file_manager.py +0 -0
  32. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/fuzzy_engine/event_log_analyser.py +0 -0
  33. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/fuzzy_engine/fuzzy_calendar.py +0 -0
  34. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/global_attributes.py +0 -0
  35. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/global_attributes_parser.py +0 -0
  36. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/histogram_distribution.py +0 -0
  37. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation.py +0 -0
  38. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation_parser.py +0 -0
  39. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/prioritisation_rules.py +0 -0
  40. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/probability_distributions.py +0 -0
  41. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/resource_profile.py +0 -0
  42. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_execution_stats.py +0 -0
  43. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_queues_ds.py +0 -0
  44. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_stats.py +0 -0
  45. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/simulation_stats_calculator.py +0 -0
  46. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/warning_logger.py +0 -0
  47. {prosimos-2.0.4 → prosimos-2.0.5}/prosimos/weekday_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prosimos
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary:
5
5
  Author: Iryna Halenok
6
6
  Author-email: iryna.halenok@ut.ee
@@ -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
- case_value = all_case_values[self.attribute]
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
- evaluator = InOperatorEvaluator(self.value, case_value)
41
- return evaluator.eval()
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 self.value == case_value
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 != 1:
59
- raise InvalidCaseAttributeException(f"Case attribute ${self.name}: probabilities' sum should be equal to 1")
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[e_info.id]
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
- condition_choice = GatewayConditionChoice(candidates_list, curr_gateway_conditions.rules_list)
30
- passed_arcs_ids = condition_choice.get_outgoing_flow(all_attributes)
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[e_info.id]
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
- condition_choice = GatewayConditionChoice(candidates_list, curr_gateway_conditions.rules_list)
64
- passed_arcs_ids = condition_choice.get_outgoing_flow(all_attributes)
65
-
66
- # All false (use probabilities)
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
- warning_logger.add_warning(f"{e_info.id} all OR gateway conditions evaluated to negative result. Probabilities were used.")
69
- return element_probability[e_info.id].get_multiple_flows()
70
- else:
71
- return [(flow, None) for flow in passed_arcs_ids]
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.global_attributes.get_values_calculated(),
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) if
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(prob_info["path_id"])
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(prob_info["path_id"])
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 len(gateway_rules) != len(g_info["probabilities"]):
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "prosimos"
3
- version = "2.0.4"
3
+ version = "2.0.5"
4
4
  description = ""
5
5
  authors = [
6
6
  "Iryna Halenok <iryna.halenok@ut.ee>",
@@ -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