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.
Files changed (45) hide show
  1. {prosimos-2.0.2 → prosimos-2.0.4}/PKG-INFO +1 -2
  2. prosimos-2.0.4/prosimos/all_attributes.py +78 -0
  3. prosimos-2.0.4/prosimos/branch_condition_parser.py +42 -0
  4. prosimos-2.0.4/prosimos/branch_condition_rules.py +85 -0
  5. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/case_attributes.py +4 -2
  6. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/control_flow_manager.py +56 -31
  7. prosimos-2.0.4/prosimos/event_attributes.py +124 -0
  8. prosimos-2.0.4/prosimos/event_attributes_parser.py +23 -0
  9. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/exceptions.py +11 -1
  10. prosimos-2.0.4/prosimos/gateway_condition_choice.py +10 -0
  11. prosimos-2.0.4/prosimos/global_attributes.py +73 -0
  12. prosimos-2.0.4/prosimos/global_attributes_parser.py +18 -0
  13. prosimos-2.0.4/prosimos/outgoing_flow_selector.py +89 -0
  14. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_engine.py +91 -50
  15. prosimos-2.0.4/prosimos/simulation_execution_stats.py +131 -0
  16. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_properties_parser.py +90 -4
  17. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_setup.py +12 -5
  18. prosimos-2.0.4/prosimos/warning_logger.py +30 -0
  19. {prosimos-2.0.2 → prosimos-2.0.4}/pyproject.toml +1 -2
  20. {prosimos-2.0.2 → prosimos-2.0.4}/README.md +0 -0
  21. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/__init__.py +0 -0
  22. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/emd_metric.py +0 -0
  23. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/exceptions.py +0 -0
  24. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/inter_arrival_cases_discovery.py +0 -0
  25. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/log_comparison_metrics.py +0 -0
  26. {prosimos-2.0.2 → prosimos-2.0.4}/bpdfr_discovery/log_parser.py +0 -0
  27. {prosimos-2.0.2 → prosimos-2.0.4}/cli/__init__.py +0 -0
  28. {prosimos-2.0.2 → prosimos-2.0.4}/cli/diff_res_bpsim.py +0 -0
  29. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/__init__.py +0 -0
  30. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/batch_processing.py +0 -0
  31. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/batch_processing_parser.py +0 -0
  32. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/execution_info.py +0 -0
  33. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/file_manager.py +0 -0
  34. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/fuzzy_engine/event_log_analyser.py +0 -0
  35. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/fuzzy_engine/fuzzy_calendar.py +0 -0
  36. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/histogram_distribution.py +0 -0
  37. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation.py +0 -0
  38. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation_parser.py +0 -0
  39. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/prioritisation_rules.py +0 -0
  40. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/probability_distributions.py +0 -0
  41. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/resource_profile.py +0 -0
  42. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_queues_ds.py +0 -0
  43. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_stats.py +0 -0
  44. {prosimos-2.0.2 → prosimos-2.0.4}/prosimos/simulation_stats_calculator.py +0 -0
  45. {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.2
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
- def set_additional_fields_from_json(self, element_probability, task_resource_probability, \
158
- event_distribution, batch_processing):
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 e_info.type is BPMN.EXCLUSIVE_GATEWAY:
332
- f_arcs = [(self.element_probability[e_info.id].get_outgoing_flow(), None)]
333
- elif e_info.type is BPMN.EVENT_BASED_GATEWAY:
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
- if e_info.type in \
337
- [BPMN.TASK, BPMN.PARALLEL_GATEWAY, BPMN.START_EVENT, BPMN.INTERMEDIATE_EVENT]:
338
- flows = copy.deepcopy(e_info.outgoing_flows)
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
+