diff --git a/pyproject.toml b/pyproject.toml index 02c44310..70a0141e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "simod" -version = "5.1.5" +version = "5.1.6" authors = [ "Ihar Suvorau ", "David Chapela ", diff --git a/src/simod/control_flow/discovery.py b/src/simod/control_flow/discovery.py index 5a350d4f..4fda675e 100644 --- a/src/simod/control_flow/discovery.py +++ b/src/simod/control_flow/discovery.py @@ -140,12 +140,11 @@ def add_bpmn_diagram_to_model(bpmn_model_path: Path): """ global bpmn_layout_jar_path - args = [ - "java", - "-jar", - '"' + str(bpmn_layout_jar_path) + '"', - '"' + str(bpmn_model_path) + '"' - ] + if is_windows(): + args = ["java", "-jar", '"' + str(bpmn_layout_jar_path) + '"', '"' + str(bpmn_model_path) + '"'] + else: + args = ["java", "-jar", str(bpmn_layout_jar_path), str(bpmn_model_path)] + print_step(f"Adding BPMN diagram to the model: {args}") execute_external_command(args) diff --git a/src/simod/simod.py b/src/simod/simod.py index 64316a89..5e5992f9 100644 --- a/src/simod/simod.py +++ b/src/simod/simod.py @@ -30,6 +30,7 @@ from simod.resource_model.repair import repair_with_missing_activities from simod.resource_model.settings import HyperoptIterationParams as ResourceModelHyperoptIterationParams from simod.runtime_meter import RuntimeMeter +from simod.settings.control_flow_settings import ProcessModelDiscoveryAlgorithm from simod.settings.simod_settings import SimodSettings from simod.simulation.parameters.BPS_model import BPSModel from simod.simulation.prosimos import simulate_and_evaluate @@ -213,7 +214,10 @@ def run(self, runtimes: Optional[RuntimeMeter] = None): ) # Instantiate event log to discover the process model with xes_log_path = self._best_result_dir / f"{self._event_log.process_name}_train_val.xes" - self._event_log.train_validation_to_xes(xes_log_path) + if best_control_flow_params.mining_algorithm is ProcessModelDiscoveryAlgorithm.SPLIT_MINER_V1: + self._event_log.train_validation_to_xes(xes_log_path, only_complete_events=True) + else: + self._event_log.train_validation_to_xes(xes_log_path) # Discover the process model discover_process_model( log_path=xes_log_path, diff --git a/tests/assets/process_model_with_SplitMiner_self_loops.bpmn b/tests/assets/process_model_with_SplitMiner_self_loops.bpmn new file mode 100644 index 00000000..c71274c4 --- /dev/null +++ b/tests/assets/process_model_with_SplitMiner_self_loops.bpmn @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +node_01d8c20e-52d6-4fbf-80d1-f7d458406138 + +node_c450dccc-8229-4e76-b957-494c44fec17e + +node_2b39336a-70f4-4dea-a952-b6f543777b5d + + + +node_4cf191ad-01b2-4038-8b26-18a164000b05 + +node_24fc93fc-4b66-4f44-9e8e-a649a3fcdc12 + +node_135b1874-4857-4b7a-a81d-50beb311441c + + + +node_cbce22a3-4f84-4bea-bc66-d6acc60d873a + +node_4cf191ad-01b2-4038-8b26-18a164000b05 + +node_6c2703c8-ee82-43e5-ba14-1721cf5138c9 + + + +node_2ea0cee5-c9bf-4d90-bf38-82614ca84894 + +node_a1fdda3a-cf96-4980-a32c-cac63028ef50 + +node_b96514e9-abb8-47da-9ce9-6ef16bd246db + +node_5679761c-c5d3-4e3a-8248-13fd7b48ab95 + + + +node_6659ae33-5879-40ee-9b4c-12113190ead1 + +node_01d8c20e-52d6-4fbf-80d1-f7d458406138 + +node_83305431-0a61-43bc-ac36-3acd90ad606c + + + +node_46301cea-0047-452b-a608-16a7e9f79cfe + +node_80986967-4875-41c2-828f-3e144d755b81 + +node_5679761c-c5d3-4e3a-8248-13fd7b48ab95 + +node_c450dccc-8229-4e76-b957-494c44fec17e + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_control_flow/test_discovery.py b/tests/test_control_flow/test_discovery.py index 12289336..159a789e 100644 --- a/tests/test_control_flow/test_discovery.py +++ b/tests/test_control_flow/test_discovery.py @@ -1,3 +1,4 @@ +import shutil import tempfile from pathlib import Path @@ -6,7 +7,7 @@ from pix_framework.discovery.gateway_probabilities import GatewayProbabilitiesDiscoveryMethod from pix_framework.io.bpmn import get_activities_names_from_bpmn -from simod.control_flow.discovery import discover_process_model +from simod.control_flow.discovery import discover_process_model, post_process_bpmn_self_loops from simod.control_flow.settings import HyperoptIterationParams from simod.settings.common_settings import Metric from simod.settings.control_flow_settings import ProcessModelDiscoveryAlgorithm @@ -103,3 +104,48 @@ def test_discover_process_model_explicit_self_loops(entry_point, test_data): # Commented because SM2 doesn't sort the events, thus no parallelism # parallel_gateways = root.findall(".//bpmn:parallelGateway", namespaces=ns) # assert len(parallel_gateways) == 2, "There should only be two parallel gateways in this model" + + +def test_transform_process_model_explicit_self_loops(entry_point): + with tempfile.TemporaryDirectory() as tmp_dir: + # Copy source model with self-loops + original_model_path = entry_point / "process_model_with_SplitMiner_self_loops.bpmn" + model_path = Path(tmp_dir) / "process_model_with_SplitMiner_self_loops.bpmn" + shutil.copy(original_model_path, model_path) + # Fix process model with self-loops in all activities except Start and End + post_process_bpmn_self_loops(model_path) + # Assert that no implicit self-loops are there + tree = etree.parse(model_path) + root = tree.getroot() + ns = {"bpmn": root.nsmap.get(None, "http://www.omg.org/spec/BPMN/20100524/MODEL")} + tasks = root.findall(".//bpmn:task", namespaces=ns) + for task in tasks: + assert task.find( + "bpmn:standardLoopCharacteristics", + namespaces=ns + ) is None, f"Task '{task.get('name')}' has an implicit self loop" + if task.get("name") == "Start": + # Find the incoming flow of the "Start" task + task_id = task.get("id") + sequence_flows = root.findall(".//bpmn:sequenceFlow", namespaces=ns) + incoming_flows = [flow for flow in sequence_flows if flow.get("targetRef") == task_id] + assert len(incoming_flows) == 1, f"Task 'Start' should have exactly one incoming flow" + # Assert that the source element of the incoming flow is the start event + incoming_flow_source = incoming_flows[0].get("sourceRef") + start_events = root.findall(".//bpmn:startEvent", namespaces=ns) + start_event_ids = {event.get("id") for event in start_events} + assert incoming_flow_source in start_event_ids, f"'Start' task was modified." + elif task.get("name") == "End": + # Find the outgoing flow of the "End" task + task_id = task.get("id") + sequence_flows = root.findall(".//bpmn:sequenceFlow", namespaces=ns) + outgoing_flows = [flow for flow in sequence_flows if flow.get("sourceRef") == task_id] + assert len(outgoing_flows) == 1, f"Task 'End' should have exactly one outgoing flow" + # Assert that the target element of the outgoing flow is the end event + outgoing_flow_target = outgoing_flows[0].get("targetRef") + end_events = root.findall(".//bpmn:endEvent", namespaces=ns) + end_event_ids = {event.get("id") for event in end_events} + assert outgoing_flow_target in end_event_ids, f"'End' task was modified." + # Verify number of gateways is original + 2 per self-loop activity + exclusive_gateways = root.findall(".//bpmn:exclusiveGateway", namespaces=ns) + assert len(exclusive_gateways) == 18, "There should only be 18 exclusive gateways in this model"