Skip to content

Integration of bpmn2constraints into Declare4Py #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,5 @@ dmypy.json
*.lp
*.asp

# vsc files
.vscode
103 changes: 103 additions & 0 deletions Declare4Py/ProcessModels/BpmnConstraintsModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import io
import logging
import sys
import json
from pathlib import Path
from tqdm import tqdm
from Declare4Py.Utils.bpmnconstraints.parser.bpmn_parser import Parser
from Declare4Py.Utils.bpmnconstraints.compiler.bpmn_compiler import Compiler
from Declare4Py.ProcessModels.DeclareModel import DeclareModel
from Declare4Py.ProcessModels.LTLModel import LTLModel
from Declare4Py.ProcessModels.AbstractModel import ProcessModel
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

class BpmnConstraintsModel(ProcessModel):
"""
BpmnConstraintsModel is designed to extract constraints from a bpmn diagram expressed as a xml file.
For example usage, see Declare4Py/Utils/bpmnconstraints/tutorial/bpmn2constraints.ipynb
"""
def __init__(self):
super().__init__()
self.declare_model = DeclareModel()
self.ltl_model = []
self.formulas = []
self.serialized_constraints: [str] = []

def parse_from_file(self, model_path: str, **kwargs):
"""

Uses a path to a xml file to parse and create a DeclareModel and a LTLModel for Declare4Py

arguments:
modelpath - The path to the xml file, as a string
kwargs -
"""
xml_path = Path(model_path)

# Check if the path is a file
if not xml_path.is_file():
logging.warning("Provided path is not a file: {}".format(model_path))
return
try:
# Parsing and compiling the BPMN constraints
parsed_result = Parser(xml_path, True, transitivity=False).run()
compiled_result = Compiler(parsed_result, transitivity=True, skip_named_gateways=False).run()

# Extract DECLARE and LTLf constraints
declare_constraints = [constraint.get("DECLARE") for constraint in compiled_result]
ltl_constraints = [constraint.get("LTLf") for constraint in compiled_result]

# Assign the constraints to the class attributes if they exist
if declare_constraints and ltl_constraints:
self.declare_model = self.declare_model.parse_from_diagram(declare_constraints)
for con in ltl_constraints:
if con is not None:
ltl_model = LTLModel()
ltl_model.parse_from_diagram(con, self.declare_model.activities)
self.ltl_model.append(ltl_model)


except Exception as e:
logging.error(f"{model_path}: {e}")

def parse_from_string(self, content: str, **kwargs):
declare_constraints = []
ltl_constraints = []

# Convert string content to a StringIO object to mimic a file-like object
xml_io = io.StringIO(content)

try:
# Parse and compile the BPMN constraints from the string
result = Parser(xml_io, True, transitivity=False).run()
result = Compiler(result, transitivity=False, skip_named_gateways=False).run()

# Extract DECLARE and LTLf constraints
for constraint in tqdm(result):
declare_constraints.append(constraint.get("DECLARE"))
ltl_constraints.append(constraint.get("LTLf"))

# Assign the parsed constraints to the class attributes
if declare_constraints:
self.declare_model = declare_constraints
self.ltl_model = ltl_constraints
except Exception as e:
logging.error(f"Error parsing from string: {e}")

def to_file(self, model_path: str, **kwargs):
data = {
"declare_model": self.declare_model,
"ltl_model": self.ltl_model
}
with open(model_path, 'w') as file:
json.dump(data, file, indent=4)
logging.info(f"Model saved to {model_path}")

def set_constraints(self):
"""Sets the constraints for the Declare model"""
for constraint in self.constraints:
constraint_str = constraint['template'].templ_str
if constraint['template'].supports_cardinality:
constraint_str += str(constraint['n'])
constraint_str += '[' + ", ".join(constraint["activities"]) + '] |' + ' |'.join(constraint["condition"])
self.serialized_constraints.append(constraint_str)
43 changes: 43 additions & 0 deletions Declare4Py/ProcessModels/DeclareModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,49 @@ def parse_from_file(self, filename: str, **kwargs) -> DeclareModel:
self.declare_model_lines = lines
self.parse(lines)
return self

def parse_from_diagram(self, diagram_lines: [str]) -> DeclareModel:
"""
Parses a xml file generated from a bpmn diagram.
args:
diagram_lines: A list of declare constraints generated from a bpmn diagram

Returns:
DeclareModel
"""
all_activities = set()
constraints = {}

# Identify constraints and apply templates
for line in diagram_lines:
template_split = line.split("[", 1)
template_search = re.search(r'(^.+?)(\d*$)', template_split[0])
parts = line.strip('[]').split('[')
_, actions = parts
activities = []
for action in actions.split(', '):
activities.append(action) # Extract the activities for the current constraint
all_activities.add(action) # Extract the activity for the entire model
constraints[action] = set()

if template_search is not None:
parts = line.strip('[]').split('[')

template_str, cardinality = template_search.groups()
template = DeclareModelTemplate.get_template_from_string(template_str)
if template is not None:
# bpmn2constraints don't use conditions
tmp = {"template": template, "activities": activities,
"condition": ["",""]}#re.split(r'\s+\|', line)[1:]}
if template.supports_cardinality:
tmp['n'] = 1 if not cardinality else int(cardinality)
cardinality = tmp['n']
self.constraints.append(tmp)
self.parsed_model.add_template(line, template, str(cardinality))
self.activities = list(all_activities)
self.set_constraints()

return self

def parse(self, lines: [str]):
declare_parsed_model = self.parsed_model
Expand Down
23 changes: 23 additions & 0 deletions Declare4Py/ProcessModels/LTLModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,30 @@ def check_satisfiability(self, minimize_automaton: bool = True) -> bool:
return True
else:
return False

def parse_from_diagram(self, line: str, activites):
"""
Parses a xml file generated from a bpmn diagram.
args:
line - A string containing the LTLf formula to parse
activities - A list of activities generated from the DeclareModel

Returns:
Void
"""
for word in activites:
""" TODO: While this works currently, som modifications should be made to either parse_from_string or
the analyzer to make it applicable to all event logs """
prefixed_word = 'con_' + word.replace(' ', '').lower()
line = line.replace(word.replace(' ', '_'), prefixed_word)
if line == word:
line = word.replace(' ', '').lower()
line = 'con_' + line

self.parse_from_string(line)
self.attribute_type = ["concept:name"]


def parse_from_string(self, content: str, new_line_ctrl: str = "\n") -> None:
"""
This function expects an LTL formula as a string.
Expand Down
13 changes: 13 additions & 0 deletions Declare4Py/Utils/bpmnconstraints/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# BPMN2Constraints

Tool for compiling BPMN models directly to constraints.
Currently, BPMN2Constraints can compile BPMN models stored in both `JSON` and `XML`
format and output to `DECLARE`, `SIGNAL`
and `Linear Temporal Logic on Finite Traces` (LTLf).
BPMN2Constraints can also compile BPMN diagrams to Mermaid.js compatible flowcharts.

The original repository is available [here](https://github.com/signavio/bpmn2constraints),
and is developed by SAP Signavio.

A tutorial that provides a walk-through of how to use the tool in
an SAP Signavio context is provided [here](./tutorial/tutorial.ipynb).
91 changes: 91 additions & 0 deletions Declare4Py/Utils/bpmnconstraints/bpmnconstraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import logging
import sys
from pathlib import Path
from json import dumps
from tqdm import tqdm
from Declare4Py.bpmnconstraints.parser.bpmn_parser import Parser
from Declare4Py.bpmnconstraints.compiler.bpmn_compiler import Compiler
from Declare4Py.bpmnconstraints.utils.script_utils import Setup
from Declare4Py.bpmnconstraints.script_utils.constraint_comparison import ComparisonScript
from Declare4Py.bpmnconstraints.script_utils.dataset_parsing import ParserScript
from Declare4Py.bpmnconstraints.script_utils.dataset_compiling import CompilingScript

logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)


def bpmnconstraints(parse_path=None, compile_path=None, transitivity=False, compare_constraints=None,
dataset=None, dataframe=None, parse_dataset=None, plot=False,
constraint_type="DECLARE", compile_dataset=None, skip_named_gateways=False):
"""
Main function for BPMN2Constraints tool. At least one flag should be set.

Parameters:
- parse_path (str): Path to the BPMN file to parse.
- compile_path (str): Path to the BPMN file to compile.
- transitivity (bool): Flag for transitivity in compilation.
- compare_constraints (bool): Flag for comparing constraints.
- dataset (str): Path to the dataset for comparison.
- dataframe (str): Path to the dataframe of compiled constraints for comparison.
- parse_dataset (str): Path to the dataset folder for parsing.
- plot (bool): Flag for generating plots.
- constraint_type (str): Type of constraint to be generated ("DECLARE" or "LTLf").
- compile_dataset (str): Path to the dataset folder for compilation.
- skip_named_gateways (bool): Flag to skip adding gateways as tokens in compilation.

Returns:
- list or dict: Depending on the operation, returns a list of constraints or the result of the operation.
"""
constraints = []
if parse_path:
path = Path(parse_path)
setup = Setup(None)
if setup.is_file(path):
result = Parser(path, True, transitivity).run()
if result:
print(dumps(result, indent=2))

elif compile_path:
path = Path(compile_path)
setup = Setup(None)
if setup.is_file(path):
result = Parser(path, True, transitivity).run()
result = Compiler(result, transitivity, skip_named_gateways).run()
if constraint_type == "LTLf":
for constraint in tqdm(result):
constraints.append(constraint.get("LTLf"))
elif constraint_type == "DECLARE":
for constraint in tqdm(result):
constraints.append(constraint.get("DECLARE"))
else:
logging.warning(
"Unknown constraint type. 'DECLARE' or 'LTLF'."
)
if result:
return constraints

elif compare_constraints:
if dataset is None or dataframe is None:
logging.warning("If invoking compare_constrains, include path to dataset and dataframe")
dataframe_path = Path(dataframe)
dataset_path = Path(dataset)
setup = Setup(None)
if setup.is_file(dataframe_path) and setup.is_file(dataset_path):
script = ComparisonScript(dataset_path, dataframe_path, plot)
script.run()

elif parse_dataset:
dataset_path = Path(parse_dataset)
setup = Setup(None)
if setup.is_directory(dataset_path):
script = ParserScript(dataset_path, plot)
script.run()

elif compile_dataset:
dataset_path = Path(compile_dataset)
setup = Setup(None)
if setup.is_directory(dataset_path):
script = CompilingScript(dataset_path, transitivity, False)
script.run()

else:
print("Invalid or missing arguments.")
Loading