prosimos 2.0.2__tar.gz → 2.0.4__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.2 → prosimos-2.0.4}/PKG-INFO +1 -2
- prosimos-2.0.4/prosimos/all_attributes.py +78 -0
- prosimos-2.0.4/prosimos/branch_condition_parser.py +42 -0
- prosimos-2.0.4/prosimos/branch_condition_rules.py +85 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/case_attributes.py +4 -2
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/control_flow_manager.py +56 -31
- prosimos-2.0.4/prosimos/event_attributes.py +124 -0
- prosimos-2.0.4/prosimos/event_attributes_parser.py +23 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/exceptions.py +11 -1
- prosimos-2.0.4/prosimos/gateway_condition_choice.py +10 -0
- prosimos-2.0.4/prosimos/global_attributes.py +73 -0
- prosimos-2.0.4/prosimos/global_attributes_parser.py +18 -0
- prosimos-2.0.4/prosimos/outgoing_flow_selector.py +89 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_engine.py +91 -50
- prosimos-2.0.4/prosimos/simulation_execution_stats.py +131 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_properties_parser.py +90 -4
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_setup.py +12 -5
- prosimos-2.0.4/prosimos/warning_logger.py +30 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/pyproject.toml +1 -2
- {prosimos-2.0.2 → prosimos-2.0.4}/README.md +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/__init__.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/emd_metric.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/exceptions.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/inter_arrival_cases_discovery.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/log_comparison_metrics.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/log_parser.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/cli/__init__.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/cli/diff_res_bpsim.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/__init__.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/batch_processing.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/batch_processing_parser.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/execution_info.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/file_manager.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/fuzzy_engine/event_log_analyser.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/fuzzy_engine/fuzzy_calendar.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/histogram_distribution.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation_parser.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation_rules.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/probability_distributions.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/resource_profile.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_queues_ds.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_stats.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_stats_calculator.py +0 -0
- {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/weekday_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: prosimos
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Iryna Halenok
|
|
6
6
|
Author-email: iryna.halenok@ut.ee
|
|
@@ -13,7 +13,6 @@ Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
|
13
13
|
Requires-Dist: numpy (>=1.24.3,<2.0.0)
|
|
14
14
|
Requires-Dist: pandas (>=2.0.1,<3.0.0)
|
|
15
15
|
Requires-Dist: pix-framework (>=0.13.0,<0.14.0)
|
|
16
|
-
Requires-Dist: pm4py (>=2.7.4,<3.0.0)
|
|
17
16
|
Requires-Dist: pylint (>=2.17.4,<3.0.0)
|
|
18
17
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
19
18
|
Requires-Dist: pytz (>=2023.3,<2024.0)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from prosimos.event_attributes import AllEventAttributes
|
|
2
|
+
from prosimos.case_attributes import AllCaseAttributes
|
|
3
|
+
from prosimos.global_attributes import AllGlobalAttributes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AllAttributes:
|
|
7
|
+
def __init__(self, global_attributes, case_attributes, event_attributes):
|
|
8
|
+
global_attribute_names = {attr for attr in global_attributes.attributes}
|
|
9
|
+
all_case_attribute_names = {attr.name for attr in case_attributes.attributes}
|
|
10
|
+
all_event_attribute_names = set()
|
|
11
|
+
for event in event_attributes.attributes:
|
|
12
|
+
attrs = event_attributes.attributes[event]
|
|
13
|
+
all_event_attribute_names.update(attrs.keys())
|
|
14
|
+
|
|
15
|
+
global_case_attribute_names = global_attribute_names.intersection(all_case_attribute_names)
|
|
16
|
+
global_event_attribute_names = global_attribute_names.intersection(all_event_attribute_names)
|
|
17
|
+
|
|
18
|
+
self.global_attributes = self._extract_global_attributes(global_attributes, global_case_attribute_names, global_event_attribute_names)
|
|
19
|
+
|
|
20
|
+
self.global_case_attributes, self.case_attributes = \
|
|
21
|
+
self._extract_case_attributes(case_attributes, global_case_attribute_names)
|
|
22
|
+
|
|
23
|
+
self.global_event_attributes, self.event_attributes = \
|
|
24
|
+
self._extract_event_attributes(event_attributes, global_event_attribute_names)
|
|
25
|
+
|
|
26
|
+
self.event_attribute_names = [attr_name for inner_dict in self.event_attributes.attributes.values() for attr_name in inner_dict.keys()]
|
|
27
|
+
|
|
28
|
+
local_case_attribute_names = all_case_attribute_names - global_case_attribute_names
|
|
29
|
+
local_event_attribute_names = all_event_attribute_names - global_event_attribute_names
|
|
30
|
+
attributes_intersection = local_case_attribute_names.intersection(local_event_attribute_names)
|
|
31
|
+
if attributes_intersection:
|
|
32
|
+
raise ValueError(f"Case attributes cannot be changed by event attributes. Check {attributes_intersection} attributes")
|
|
33
|
+
|
|
34
|
+
def get_all_columns_generated(self):
|
|
35
|
+
attributes_collections = [
|
|
36
|
+
self.global_attributes,
|
|
37
|
+
self.global_case_attributes,
|
|
38
|
+
self.global_event_attributes,
|
|
39
|
+
self.case_attributes,
|
|
40
|
+
self.event_attributes
|
|
41
|
+
]
|
|
42
|
+
columns = [attr.get_columns_generated() for attr in attributes_collections]
|
|
43
|
+
return [item for sublist in columns for item in sorted(sublist)]
|
|
44
|
+
|
|
45
|
+
def _extract_case_attributes(self, case_attributes, global_case_attribute_names):
|
|
46
|
+
global_case_attributes = list()
|
|
47
|
+
local_case_attributes = list()
|
|
48
|
+
|
|
49
|
+
for attr in case_attributes.attributes:
|
|
50
|
+
if attr.name in global_case_attribute_names:
|
|
51
|
+
global_case_attributes.append(attr)
|
|
52
|
+
else:
|
|
53
|
+
local_case_attributes.append(attr)
|
|
54
|
+
|
|
55
|
+
return AllCaseAttributes(global_case_attributes), AllCaseAttributes(local_case_attributes)
|
|
56
|
+
|
|
57
|
+
def _extract_event_attributes(self, event_attributes, global_event_attribute_names):
|
|
58
|
+
global_event_attributes = {}
|
|
59
|
+
local_event_attributes = {}
|
|
60
|
+
|
|
61
|
+
for event, attrs in event_attributes.attributes.items():
|
|
62
|
+
for attr, value in attrs.items():
|
|
63
|
+
if attr in global_event_attribute_names:
|
|
64
|
+
global_event_attributes.setdefault(event, {})[attr] = value
|
|
65
|
+
else:
|
|
66
|
+
local_event_attributes.setdefault(event, {})[attr] = value
|
|
67
|
+
return AllEventAttributes(global_event_attributes), AllEventAttributes(local_event_attributes)
|
|
68
|
+
|
|
69
|
+
def _extract_global_attributes(self, global_attributes, global_case_attribute_names, global_event_attribute_names):
|
|
70
|
+
attributes_to_remove = global_case_attribute_names.union(global_event_attribute_names)
|
|
71
|
+
|
|
72
|
+
filtered_attributes_dict = {
|
|
73
|
+
attr_name: attr_object
|
|
74
|
+
for attr_name, attr_object in global_attributes.attributes.items()
|
|
75
|
+
if attr_name not in attributes_to_remove
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return AllGlobalAttributes(filtered_attributes_dict)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from prosimos.branch_condition_rules import (
|
|
3
|
+
AllBranchConditionRules,
|
|
4
|
+
AndBranchConditionRule,
|
|
5
|
+
OrBranchConditionRule,
|
|
6
|
+
BranchConditionRule,
|
|
7
|
+
BranchConditionWithRule
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class BranchConditionParser:
|
|
11
|
+
def __init__(self, json_data_with_branching_conditions):
|
|
12
|
+
self.data = json_data_with_branching_conditions
|
|
13
|
+
|
|
14
|
+
def parse(self):
|
|
15
|
+
branching_conditions: List[BranchConditionWithRule] = []
|
|
16
|
+
for curr_branch_condition_rule in self.data:
|
|
17
|
+
or_rules_json = curr_branch_condition_rule["rules"]
|
|
18
|
+
branch_id = curr_branch_condition_rule["id"]
|
|
19
|
+
|
|
20
|
+
# print(f"OR RULES JSON: {str(or_rules_json)}")
|
|
21
|
+
or_rules: List[AndBranchConditionRule] = []
|
|
22
|
+
for or_rule in or_rules_json:
|
|
23
|
+
and_rules: List[BranchConditionRule] = []
|
|
24
|
+
# print(f"OR RULE: {str(or_rule)}")
|
|
25
|
+
for and_rule_json in or_rule:
|
|
26
|
+
# print(f"AND RULE JSON: {str(and_rule_json)}")
|
|
27
|
+
rule: BranchConditionRule = BranchConditionRule(
|
|
28
|
+
and_rule_json["attribute"],
|
|
29
|
+
and_rule_json["comparison"],
|
|
30
|
+
and_rule_json["value"],
|
|
31
|
+
)
|
|
32
|
+
and_rules.append(rule)
|
|
33
|
+
# print(f"AND RULES: {str(or_rule)}")
|
|
34
|
+
all_and_rules = AndBranchConditionRule(and_rules)
|
|
35
|
+
or_rules.append(all_and_rules)
|
|
36
|
+
|
|
37
|
+
all_or_rules = OrBranchConditionRule(or_rules)
|
|
38
|
+
# print(f"ALL OR RULES: {all_or_rules}\n\n")
|
|
39
|
+
|
|
40
|
+
branching_conditions.append(BranchConditionWithRule(all_or_rules, branch_id))
|
|
41
|
+
|
|
42
|
+
return AllBranchConditionRules(branching_conditions)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InOperatorEvaluator:
|
|
6
|
+
def __init__(self, range: Tuple[float, float], case_value: float):
|
|
7
|
+
self.min, self.max = range # including the edges
|
|
8
|
+
self.value = case_value
|
|
9
|
+
|
|
10
|
+
def eval(self):
|
|
11
|
+
if self.min == self.max:
|
|
12
|
+
# equal operator
|
|
13
|
+
return self.min == self.value
|
|
14
|
+
else:
|
|
15
|
+
# check whether in the range
|
|
16
|
+
return self.min <= self.value <= self.max
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BranchConditionRule:
|
|
20
|
+
def __init__(self, attribute, condition, value):
|
|
21
|
+
self.attribute = attribute
|
|
22
|
+
self.condition = condition
|
|
23
|
+
self.value = self._parse_value(value)
|
|
24
|
+
|
|
25
|
+
def _parse_value(self, value):
|
|
26
|
+
if self.condition == "in":
|
|
27
|
+
min_boundary = float(value[0])
|
|
28
|
+
max_boundary = sys.maxsize if value[1] == "inf" else float(value[1])
|
|
29
|
+
|
|
30
|
+
return min_boundary, max_boundary
|
|
31
|
+
else:
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
def is_rule_true(self, all_case_values):
|
|
35
|
+
if self.attribute not in all_case_values:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
case_value = all_case_values[self.attribute]
|
|
39
|
+
if self.condition == "in":
|
|
40
|
+
evaluator = InOperatorEvaluator(self.value, case_value)
|
|
41
|
+
return evaluator.eval()
|
|
42
|
+
else:
|
|
43
|
+
return self.value == case_value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AndBranchConditionRule:
|
|
47
|
+
def __init__(self, and_rules: List[BranchConditionRule]):
|
|
48
|
+
self.and_rules = and_rules
|
|
49
|
+
|
|
50
|
+
def is_and_rule_true(self, all_case_values):
|
|
51
|
+
init_val = True
|
|
52
|
+
for item in self.and_rules:
|
|
53
|
+
init_val = init_val and item.is_rule_true(all_case_values)
|
|
54
|
+
return init_val
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OrBranchConditionRule:
|
|
58
|
+
def __init__(self, or_rules: List[AndBranchConditionRule]):
|
|
59
|
+
self.or_rules = or_rules
|
|
60
|
+
|
|
61
|
+
def is_or_rule_true(self, all_case_values):
|
|
62
|
+
init_val = False
|
|
63
|
+
for item in self.or_rules:
|
|
64
|
+
init_val = init_val or item.is_and_rule_true(all_case_values)
|
|
65
|
+
return init_val
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BranchConditionWithRule:
|
|
69
|
+
def __init__(self, or_rule: OrBranchConditionRule, id: str):
|
|
70
|
+
self.or_rule = or_rule
|
|
71
|
+
self.id = id
|
|
72
|
+
|
|
73
|
+
def is_true(self, all_case_values):
|
|
74
|
+
return self.or_rule.is_or_rule_true(all_case_values)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AllBranchConditionRules:
|
|
78
|
+
def __init__(self, rules_array: List[BranchConditionWithRule]):
|
|
79
|
+
self.all_rules = rules_array
|
|
80
|
+
|
|
81
|
+
def get_branch_condition_by_id(self, branch_condition_id):
|
|
82
|
+
for rule in self.all_rules:
|
|
83
|
+
if rule.id == branch_condition_id:
|
|
84
|
+
return rule
|
|
85
|
+
return None
|
|
@@ -12,6 +12,7 @@ class CASE_ATTR_TYPE(Enum):
|
|
|
12
12
|
DISCRETE = "discrete"
|
|
13
13
|
CONTINUOUS = "continuous"
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
def parse_discrete_value(value_info_arr):
|
|
16
17
|
prob_arr = []
|
|
17
18
|
options_arr = []
|
|
@@ -24,11 +25,12 @@ def parse_discrete_value(value_info_arr):
|
|
|
24
25
|
"probabilities": prob_arr
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
def parse_continuous_value(value_info) -> "DurationDistribution":
|
|
28
30
|
return DurationDistribution.from_dict(value_info)
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
class CaseAttribute
|
|
33
|
+
class CaseAttribute:
|
|
32
34
|
def __init__(self, name, case_atrr_type, value):
|
|
33
35
|
self.name: str = name
|
|
34
36
|
self.case_atrr_type: CASE_ATTR_TYPE = CASE_ATTR_TYPE(case_atrr_type)
|
|
@@ -59,7 +61,7 @@ class CaseAttribute():
|
|
|
59
61
|
return True
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
class AllCaseAttributes
|
|
64
|
+
class AllCaseAttributes:
|
|
63
65
|
def __init__(self, case_attr_arr: List[CaseAttribute]):
|
|
64
66
|
self.attributes = case_attr_arr
|
|
65
67
|
|
|
@@ -12,6 +12,8 @@ from prosimos.batch_processing import (BATCH_TYPE, AndFiringRule,
|
|
|
12
12
|
BatchConfigPerTask)
|
|
13
13
|
from prosimos.exceptions import InvalidBpmnModelException
|
|
14
14
|
from prosimos.weekday_helper import CustomDatetimeAndSeconds
|
|
15
|
+
from prosimos.simulation_execution_stats import SimulationExecutionStats
|
|
16
|
+
from prosimos.warning_logger import warning_logger
|
|
15
17
|
|
|
16
18
|
seconds_per_unit = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
|
|
17
19
|
|
|
@@ -24,7 +26,7 @@ class BatchInfoForExecution:
|
|
|
24
26
|
|
|
25
27
|
def is_sequential(self):
|
|
26
28
|
return self.task_batch_info.type == BATCH_TYPE.SEQUENTIAL
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
def is_concurrent(self):
|
|
29
31
|
return self.task_batch_info.type == BATCH_TYPE.CONCURRENT
|
|
30
32
|
|
|
@@ -36,7 +38,7 @@ class EnabledTask:
|
|
|
36
38
|
def __init__(self, task_id, batch_info_exec: BatchInfoForExecution = None, duration_sec = None, is_event = False):
|
|
37
39
|
self.task_id = task_id
|
|
38
40
|
self.batch_info_exec = batch_info_exec
|
|
39
|
-
|
|
41
|
+
|
|
40
42
|
# is filled only for event-based gateways
|
|
41
43
|
# (when we already know the duration at the control flow step)
|
|
42
44
|
self.duration_sec = duration_sec
|
|
@@ -61,6 +63,8 @@ class BPMN(Enum):
|
|
|
61
63
|
else:
|
|
62
64
|
return False
|
|
63
65
|
|
|
66
|
+
from prosimos.outgoing_flow_selector import OutgoingFlowSelector
|
|
67
|
+
|
|
64
68
|
class EVENT_TYPE(Enum):
|
|
65
69
|
MESSAGE = 'MESSAGE'
|
|
66
70
|
TIMER = 'TIMER'
|
|
@@ -90,7 +94,7 @@ class ElementInfo:
|
|
|
90
94
|
|
|
91
95
|
def is_start_or_end_event(self):
|
|
92
96
|
return self.type in [BPMN.START_EVENT, BPMN.END_EVENT]
|
|
93
|
-
|
|
97
|
+
|
|
94
98
|
def is_event(self):
|
|
95
99
|
return self.type in [BPMN.START_EVENT, BPMN.END_EVENT, BPMN.INTERMEDIATE_EVENT]
|
|
96
100
|
|
|
@@ -153,13 +157,20 @@ class BPMNGraph:
|
|
|
153
157
|
self.batch_count = dict()
|
|
154
158
|
self.batch_waiting_processes = dict()
|
|
155
159
|
self.last_datetime = dict()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
self.all_attributes = None
|
|
161
|
+
self.gateway_conditions = None
|
|
162
|
+
self.gateway_execution_limit = 1000
|
|
163
|
+
self.simulation_execution_stats = SimulationExecutionStats()
|
|
164
|
+
|
|
165
|
+
def set_additional_fields_from_json(self, element_probability, task_resource_probability,
|
|
166
|
+
event_distribution, batch_processing, gateway_conditions,
|
|
167
|
+
gateway_execution_limit):
|
|
159
168
|
self.element_probability = element_probability
|
|
160
169
|
self.task_resource_probability = task_resource_probability
|
|
161
170
|
self.event_distribution = event_distribution
|
|
162
171
|
self.batch_info = batch_processing
|
|
172
|
+
self.gateway_conditions = gateway_conditions
|
|
173
|
+
self.gateway_execution_limit = gateway_execution_limit
|
|
163
174
|
|
|
164
175
|
def add_bpmn_element(self, element_id, element_info):
|
|
165
176
|
if element_info.type == BPMN.START_EVENT:
|
|
@@ -171,6 +182,7 @@ class BPMNGraph:
|
|
|
171
182
|
self.from_name[element_info.name] = element_id
|
|
172
183
|
self.nodes_bitset[element_id] = (1 << len(self.element_info))
|
|
173
184
|
self.last_datetime[element_id] = dict()
|
|
185
|
+
self.simulation_execution_stats.add_element(element_info, element_id)
|
|
174
186
|
|
|
175
187
|
def add_flow_arc(self, flow_id, source_id, target_id):
|
|
176
188
|
for node_id in [source_id, target_id]:
|
|
@@ -328,18 +340,19 @@ class BPMNGraph:
|
|
|
328
340
|
f_arcs = [(flow, None) for flow in flows]
|
|
329
341
|
|
|
330
342
|
if len(f_arcs) > 1:
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
if self.is_gateway_execution_limit_exceeded(case_id, e_info.id):
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
if e_info.type is BPMN.EVENT_BASED_GATEWAY:
|
|
334
347
|
f_arcs = [self.get_event_gateway_choice(e_info, last_enabled.datetime)]
|
|
335
348
|
else:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
f_arcs = [(flow, None) for flow in flows]
|
|
340
|
-
elif e_info.type is BPMN.INCLUSIVE_GATEWAY:
|
|
341
|
-
f_arcs = self.element_probability[e_info.id].get_multiple_flows()
|
|
349
|
+
all_curr_attributes = self.get_all_attributes(case_id)
|
|
350
|
+
f_arcs = OutgoingFlowSelector.choose_outgoing_flow(e_info, self.element_probability,
|
|
351
|
+
all_curr_attributes, self.gateway_conditions)
|
|
342
352
|
random.shuffle(f_arcs)
|
|
353
|
+
|
|
354
|
+
self.simulation_execution_stats.update_element_execution(case_id, e_info, f_arcs)
|
|
355
|
+
|
|
343
356
|
for f_arc in f_arcs:
|
|
344
357
|
self._find_next(f_arc, case_id, p_state, enabled_tasks, to_execute, last_enabled, visited_at)
|
|
345
358
|
current += 1
|
|
@@ -347,9 +360,21 @@ class BPMNGraph:
|
|
|
347
360
|
random.shuffle(enabled_tasks)
|
|
348
361
|
return enabled_tasks, visited_at
|
|
349
362
|
|
|
363
|
+
def get_all_attributes(self, case_id):
|
|
364
|
+
all_current_attributes = {}
|
|
365
|
+
all_current_attributes.update(self.all_attributes["global"])
|
|
366
|
+
all_current_attributes.update(self.all_attributes[case_id])
|
|
367
|
+
return all_current_attributes
|
|
368
|
+
|
|
350
369
|
def is_task_batched(self, task_id):
|
|
351
370
|
return self.batch_info.get(task_id, None) != None
|
|
352
371
|
|
|
372
|
+
def is_gateway_execution_limit_exceeded(self, case_id, e_id):
|
|
373
|
+
element_executions = self.simulation_execution_stats.get_element_executions(case_id, e_id)
|
|
374
|
+
if element_executions >= self.gateway_execution_limit:
|
|
375
|
+
warning_logger.add_warning(f"Infinite loop was detected in case #{case_id} on element {e_id}. Case has been terminated")
|
|
376
|
+
return True
|
|
377
|
+
return False
|
|
353
378
|
|
|
354
379
|
def is_batched_task_enabled(self, task_id: str, enabled_at: CustomDatetimeAndSeconds):
|
|
355
380
|
"""
|
|
@@ -371,8 +396,8 @@ class BPMNGraph:
|
|
|
371
396
|
# not enough items for batch execution
|
|
372
397
|
return False, None, None
|
|
373
398
|
|
|
374
|
-
waiting_time = [ (enabled_at.datetime - v.datetime).total_seconds() for (_, v) in self.batch_waiting_processes[task_id].items() ]
|
|
375
|
-
|
|
399
|
+
waiting_time = [ (enabled_at.datetime - v.datetime).total_seconds() for (_, v) in self.batch_waiting_processes[task_id].items() ]
|
|
400
|
+
|
|
376
401
|
spec = {
|
|
377
402
|
"size": size_count,
|
|
378
403
|
"waiting_times": waiting_time,
|
|
@@ -397,7 +422,7 @@ class BPMNGraph:
|
|
|
397
422
|
|
|
398
423
|
"""
|
|
399
424
|
spec["is_only_one_batch_return"] = True
|
|
400
|
-
|
|
425
|
+
|
|
401
426
|
final_is_true, final_batch_spec, final_start_time = False, None, None
|
|
402
427
|
|
|
403
428
|
while True:
|
|
@@ -415,7 +440,7 @@ class BPMNGraph:
|
|
|
415
440
|
final_batch_spec.append(num_items)
|
|
416
441
|
final_is_true = is_true
|
|
417
442
|
final_start_time = start_time
|
|
418
|
-
|
|
443
|
+
|
|
419
444
|
# regenerate the rule based on the size distribution
|
|
420
445
|
task_batch_info.update_firing_rules_from_distr()
|
|
421
446
|
|
|
@@ -427,7 +452,7 @@ class BPMNGraph:
|
|
|
427
452
|
if new_num_tasks_in_queue == 0:
|
|
428
453
|
# all items were included in the batches
|
|
429
454
|
break
|
|
430
|
-
|
|
455
|
+
|
|
431
456
|
spec["size"] = spec["size"] - num_items
|
|
432
457
|
|
|
433
458
|
return final_is_true, final_batch_spec, final_start_time
|
|
@@ -438,10 +463,10 @@ class BPMNGraph:
|
|
|
438
463
|
firing_rules: List[AndFiringRule] = task_batch_info.firing_rules
|
|
439
464
|
enabled_times = list(self.batch_waiting_processes[task_id].items())
|
|
440
465
|
batch_enabled_time = firing_rules.get_enabled_time(
|
|
441
|
-
enabled_times,
|
|
466
|
+
enabled_times,
|
|
442
467
|
last_task_enabled_time,
|
|
443
468
|
)
|
|
444
|
-
|
|
469
|
+
|
|
445
470
|
if batch_enabled_time != None:
|
|
446
471
|
enabled_time = batch_enabled_time
|
|
447
472
|
|
|
@@ -466,7 +491,7 @@ class BPMNGraph:
|
|
|
466
491
|
event_id = self.flow_arcs[outgoing_flow][1]
|
|
467
492
|
[duration] = self.event_duration(event_id)
|
|
468
493
|
all_gateway_choices[outgoing_flow] = duration
|
|
469
|
-
|
|
494
|
+
|
|
470
495
|
min_value = min(all_gateway_choices.values())
|
|
471
496
|
res = [(key, value) for key, value in all_gateway_choices.items() if value == min_value]
|
|
472
497
|
|
|
@@ -840,9 +865,9 @@ class BPMNGraph:
|
|
|
840
865
|
enabled_time = self._check_and_update_enabling_time(case_id, next_e, enabled_time)
|
|
841
866
|
visited_at[next_e] = enabled_time
|
|
842
867
|
if self.is_enabled(next_e, p_state):
|
|
843
|
-
next_el_type = self.element_info[next_e].type
|
|
868
|
+
next_el_type = self.element_info[next_e].type
|
|
844
869
|
if next_el_type == BPMN.TASK and self.is_task_batched(next_e):
|
|
845
|
-
self.enable_batch_or_task(next_e, case_id, enabled_time, enabled_tasks, duration_sec)
|
|
870
|
+
self.enable_batch_or_task(next_e, case_id, enabled_time, enabled_tasks, duration_sec)
|
|
846
871
|
elif next_el_type == BPMN.TASK:
|
|
847
872
|
enabled_tasks.append(EnabledTask(next_e, None, duration_sec))
|
|
848
873
|
elif next_el_type == BPMN.INTERMEDIATE_EVENT:
|
|
@@ -939,13 +964,13 @@ class BPMNGraph:
|
|
|
939
964
|
# keep track of case_id which are waiting for the firing rule to be true
|
|
940
965
|
if task_id not in self.batch_waiting_processes:
|
|
941
966
|
self.batch_waiting_processes[task_id] = dict()
|
|
942
|
-
|
|
967
|
+
|
|
943
968
|
# in case we execute the batch, there is no need to increase the total num
|
|
944
969
|
if case_id in self.batch_waiting_processes[task_id]:
|
|
945
970
|
return
|
|
946
|
-
|
|
971
|
+
|
|
947
972
|
self.batch_waiting_processes[task_id][case_id] = enabled_time
|
|
948
|
-
|
|
973
|
+
|
|
949
974
|
if task_id in self.batch_count:
|
|
950
975
|
self.batch_count[task_id] += 1
|
|
951
976
|
else:
|
|
@@ -990,7 +1015,7 @@ class BPMNGraph:
|
|
|
990
1015
|
num_tasks_wait_batch = len(waiting_tasks)
|
|
991
1016
|
if num_tasks_wait_batch == 0:
|
|
992
1017
|
continue
|
|
993
|
-
|
|
1018
|
+
|
|
994
1019
|
if self.is_or_rule_invalid(waiting_tasks, task_id, num_tasks_wait_batch, current_point_of_time):
|
|
995
1020
|
# if the rule become invlaid, we execute everything we have
|
|
996
1021
|
# invalid = the one that cannot be satisfied in the future anymore
|
|
@@ -1008,7 +1033,7 @@ class BPMNGraph:
|
|
|
1008
1033
|
task_id, batch_spec, start_time_from_rule
|
|
1009
1034
|
)
|
|
1010
1035
|
continue
|
|
1011
|
-
|
|
1036
|
+
|
|
1012
1037
|
return enabled_task_batch
|
|
1013
1038
|
|
|
1014
1039
|
def create_batch_info_and_clear_from_queue(self, task_id: str, batch_spec, start_time_from_rule):
|
|
@@ -1024,7 +1049,7 @@ class BPMNGraph:
|
|
|
1024
1049
|
|
|
1025
1050
|
def is_or_rule_invalid(self, waiting_tasks, task_id: str, num_tasks_wait_batch: int, current_point_of_time: CustomDatetimeAndSeconds):
|
|
1026
1051
|
all_keys = list(waiting_tasks.keys())
|
|
1027
|
-
|
|
1052
|
+
|
|
1028
1053
|
first_key = all_keys[0]
|
|
1029
1054
|
first_wt = (current_point_of_time.datetime - waiting_tasks[first_key].datetime).seconds
|
|
1030
1055
|
|
|
@@ -0,0 +1,124 @@
|
|
|
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()}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from prosimos.event_attributes import (AllEventAttributes, EventAttribute)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EventAttributesParser:
|
|
5
|
+
def __init__(self, json_data_with_event_attributes):
|
|
6
|
+
self.data = json_data_with_event_attributes
|
|
7
|
+
|
|
8
|
+
def parse(self):
|
|
9
|
+
event_attributes = {}
|
|
10
|
+
for curr_event_attr in self.data:
|
|
11
|
+
event_id = curr_event_attr["event_id"]
|
|
12
|
+
event_attributes[event_id] = {}
|
|
13
|
+
for attr in curr_event_attr["attributes"]:
|
|
14
|
+
event_attr = EventAttribute(
|
|
15
|
+
event_id,
|
|
16
|
+
attr["name"],
|
|
17
|
+
attr["type"],
|
|
18
|
+
attr["values"]
|
|
19
|
+
)
|
|
20
|
+
event_attributes[event_id][attr["name"]] = event_attr
|
|
21
|
+
|
|
22
|
+
return AllEventAttributes(event_attributes)
|
|
23
|
+
|
|
@@ -12,12 +12,22 @@ class InvalidBpmnModelException(Error):
|
|
|
12
12
|
class InvalidLogFileException(Error):
|
|
13
13
|
"""Raised when the provided log file is invalid"""
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
class InvalidSimScenarioException(Error):
|
|
16
17
|
"""Raised when the provided simulation scenario (.json file) is invalid"""
|
|
17
18
|
|
|
19
|
+
|
|
18
20
|
class InvalidRuleDefinitionException(Error):
|
|
19
21
|
"""Raised when the defined rule is invalid"""
|
|
20
22
|
|
|
23
|
+
|
|
21
24
|
class InvalidCaseAttributeException(Error):
|
|
22
25
|
"""Raised when the defined case attribute is invalid"""
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InvalidEventAttributeException(Error):
|
|
29
|
+
"""Raised when the defined event attribute is invalid"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvalidGlobalAttributeException(Error):
|
|
33
|
+
"""Raised when the defined event attribute is invalid"""
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
|