Source code for pm4py.objects.bpmn.util.reduction

'''
PM4Py – A Process Mining Library for Python
Copyright (C) 2026 Process Intelligence Solutions GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see this software project's root or
visit <https://www.gnu.org/licenses/>.

Website: https://processintelligence.solutions
Contact: info@processintelligence.solutions
'''
from enum import Enum

from pm4py.objects.bpmn.obj import BPMN
from pm4py.util import exec_utils


[docs] class Parameters(Enum): COLLAPSE_GATEWAYS = "collapse_gateways"
# Gateway classes for which two consecutive same-type gateways can be # merged without changing the language of the model. XOR and AND are # associative (``XOR(a, XOR(b, c)) = XOR(a, b, c)`` and likewise for # AND), so flattening a nested same-type split/join preserves the set of # accepted traces. Inclusive (OR) gateways are intentionally excluded to # keep the merge provably language preserving. _MERGEABLE_GATEWAYS = (BPMN.ExclusiveGateway, BPMN.ParallelGateway) def _edge_maps(bpmn_graph): """Return ``(outgoing, incoming)`` flow maps keyed by node. Both maps associate a node with the *set* of sequence flows leaving (resp. entering) it. They are rebuilt from scratch whenever the graph is mutated so callers never act on stale adjacency. """ outgoing = {} incoming = {} for flow in bpmn_graph.get_flows(): source = flow.get_source() target = flow.get_target() outgoing.setdefault(source, set()).add(flow) incoming.setdefault(target, set()).add(flow) return outgoing, incoming
[docs] def reduce_xor_gateways(bpmn_graph, parameters=None): """ Reduces the number of XOR gateways in the diagram Parameters ------------ bpmn_graph BPMN graph parameters Parameters Returns ------------ bpmn_graph (possibly reduced) BPMN graph """ if parameters is None: parameters = {} changed = True while changed: changed = False outgoing_edges = None incoming_edges = None outgoing_edges = {} incoming_edges = {} for flow in bpmn_graph.get_flows(): source = flow.get_source() target = flow.get_target() if source not in outgoing_edges: outgoing_edges[source] = set() outgoing_edges[source].add(flow) if target not in incoming_edges: incoming_edges[target] = set() incoming_edges[target].add(flow) nodes = list(bpmn_graph.get_nodes()) for node in nodes: if isinstance(node, BPMN.ExclusiveGateway): if ( node in outgoing_edges and node in incoming_edges and len(outgoing_edges[node]) == 1 and len(incoming_edges[node]) == 1 ): changed = True source_node = None target_node = None for flow in incoming_edges[node]: source_node = flow.get_source() if flow in bpmn_graph.get_flows(): bpmn_graph.remove_flow(flow) for flow in outgoing_edges[node]: target_node = flow.get_target() if flow in bpmn_graph.get_flows(): bpmn_graph.remove_flow(flow) if node in bpmn_graph.get_nodes(): bpmn_graph.remove_node(node) bpmn_graph.add_flow( BPMN.SequenceFlow(source_node, target_node) ) break return bpmn_graph
[docs] def remove_trivial_gateways(bpmn_graph, parameters=None): """Remove gateways with exactly one incoming and one outgoing flow. A gateway with a single input and a single output neither branches nor synchronises anything, so it can be spliced out and its predecessor connected straight to its successor. This holds for every gateway type (XOR, AND and OR alike), hence the reduction is always language preserving. """ if parameters is None: parameters = {} changed = True while changed: changed = False outgoing, incoming = _edge_maps(bpmn_graph) for node in list(bpmn_graph.get_nodes()): if not isinstance(node, BPMN.Gateway): continue ins = incoming.get(node, set()) outs = outgoing.get(node, set()) if len(ins) != 1 or len(outs) != 1: continue in_flow = next(iter(ins)) out_flow = next(iter(outs)) source = in_flow.get_source() target = out_flow.get_target() bpmn_graph.remove_flow(in_flow) bpmn_graph.remove_flow(out_flow) bpmn_graph.remove_node(node) # Do not introduce a self-loop if the gateway sat on a # one-node cycle; just drop it in that degenerate case. if source is not target: bpmn_graph.add_flow(BPMN.SequenceFlow(source, target)) changed = True break return bpmn_graph
[docs] def collapse_split_gateways(bpmn_graph, parameters=None): """Merge a split gateway into a same-type successor split gateway. When a split gateway ``g`` has an outgoing flow to another split gateway ``s`` of the same type and ``s`` has no other predecessor than ``g``, the nesting ``g -> s`` is flattened: ``g`` absorbs every outgoing branch of ``s`` and ``s`` is removed. This mirrors the consecutive-split collapse performed by the reference Split Miner implementation and is restricted to XOR/AND gateways so it stays language preserving. """ if parameters is None: parameters = {} changed = True while changed: changed = False outgoing, incoming = _edge_maps(bpmn_graph) for g in list(bpmn_graph.get_nodes()): if not isinstance(g, _MERGEABLE_GATEWAYS): continue merged = False for out_flow in list(outgoing.get(g, set())): succ = out_flow.get_target() if type(succ) is not type(g): continue succ_in = incoming.get(succ, set()) # the successor split must be fed exclusively by g if any(f.get_source() is not g for f in succ_in): continue # g absorbs the successor's outgoing branches for sf in list(outgoing.get(succ, set())): tgt = sf.get_target() bpmn_graph.remove_flow(sf) if tgt is not g: bpmn_graph.add_flow(BPMN.SequenceFlow(g, tgt)) for sf in list(succ_in): bpmn_graph.remove_flow(sf) bpmn_graph.remove_node(succ) merged = True changed = True break if merged: break return bpmn_graph
[docs] def collapse_join_gateways(bpmn_graph, parameters=None): """Merge a join gateway into a same-type predecessor join gateway. Dual of :func:`collapse_split_gateways`. When a join gateway ``g`` is fed by another join gateway ``p`` of the same type and ``p``'s only successor is ``g``, the nesting ``p -> g`` is flattened: ``g`` absorbs every incoming flow of ``p`` and ``p`` is removed. Restricted to XOR/AND gateways to remain language preserving. """ if parameters is None: parameters = {} changed = True while changed: changed = False outgoing, incoming = _edge_maps(bpmn_graph) for g in list(bpmn_graph.get_nodes()): if not isinstance(g, _MERGEABLE_GATEWAYS): continue merged = False for in_flow in list(incoming.get(g, set())): pred = in_flow.get_source() if type(pred) is not type(g): continue pred_out = outgoing.get(pred, set()) # the predecessor join must feed only g if any(f.get_target() is not g for f in pred_out): continue # g absorbs the predecessor's incoming flows for pf in list(incoming.get(pred, set())): src = pf.get_source() bpmn_graph.remove_flow(pf) if src is not g: bpmn_graph.add_flow(BPMN.SequenceFlow(src, g)) for pf in list(pred_out): bpmn_graph.remove_flow(pf) bpmn_graph.remove_node(pred) merged = True changed = True break if merged: break return bpmn_graph
[docs] def collapse_gateways(bpmn_graph, parameters=None): """Flatten nested same-type gateways and drop trivial ones. Splits and joins are collapsed in alternation (split, join, split, join) because collapsing one side can expose new opportunities on the other; a final pass removes any single-in/single-out gateway left behind. All steps preserve the language of the model. """ if parameters is None: parameters = {} collapse_split_gateways(bpmn_graph, parameters=parameters) collapse_join_gateways(bpmn_graph, parameters=parameters) collapse_split_gateways(bpmn_graph, parameters=parameters) collapse_join_gateways(bpmn_graph, parameters=parameters) remove_trivial_gateways(bpmn_graph, parameters=parameters) return bpmn_graph
[docs] def apply(bpmn_graph, parameters=None): """ Reduce the complexity of a BPMN graph by removing useless elements Parameters ------------ bpmn_graph BPMN graph parameters Parameters of the algorithm, including: - Parameters.COLLAPSE_GATEWAYS: when set to ``True``, also flatten nested same-type (XOR/AND) split and join gateways. Disabled by default so existing callers keep the conservative behaviour. Returns ------------ bpmn_graph (possibly reduced) BPMN graph """ if parameters is None: parameters = {} bpmn_graph = reduce_xor_gateways(bpmn_graph, parameters=parameters) if exec_utils.get_param_value( Parameters.COLLAPSE_GATEWAYS, parameters, False ): bpmn_graph = collapse_gateways(bpmn_graph, parameters=parameters) return bpmn_graph