diff --git a/pm4py/objects/ocel/constants.py b/pm4py/objects/ocel/constants.py index 8e6b3ae8a..a917c3814 100644 --- a/pm4py/objects/ocel/constants.py +++ b/pm4py/objects/ocel/constants.py @@ -24,6 +24,10 @@ OCEL_TYPED_OMAP_KEY = "ocel:typedOmap" OCEL_VMAP_KEY = "ocel:vmap" OCEL_OVMAP_KEY = "ocel:ovmap" +OCEL_O2O_KEY = "ocel:o2o" +OCEL_OBJCHANGES_KEY = "ocel:objectChanges" +OCEL_EVTYPES_KEY = "ocel:eventTypes" +OCEL_OBJTYPES_KEY = "ocel:objectTypes" OCEL_GLOBAL_LOG = "ocel:global-log" OCEL_GLOBAL_LOG_ATTRIBUTE_NAMES = "ocel:attribute-names" OCEL_GLOBAL_LOG_OBJECT_TYPES = "ocel:object-types" diff --git a/pm4py/objects/ocel/exporter/jsonocel/exporter.py b/pm4py/objects/ocel/exporter/jsonocel/exporter.py index 4cbae1b2a..bc2236588 100644 --- a/pm4py/objects/ocel/exporter/jsonocel/exporter.py +++ b/pm4py/objects/ocel/exporter/jsonocel/exporter.py @@ -1,13 +1,14 @@ from enum import Enum from typing import Optional, Dict, Any -from pm4py.objects.ocel.exporter.jsonocel.variants import classic +from pm4py.objects.ocel.exporter.jsonocel.variants import classic, ocel20 from pm4py.objects.ocel.obj import OCEL from pm4py.util import exec_utils class Variants(Enum): CLASSIC = classic + OCEL20 = ocel20 def apply(ocel: OCEL, target_path: str, variant=Variants.CLASSIC, parameters: Optional[Dict[Any, Any]] = None): diff --git a/pm4py/objects/ocel/exporter/jsonocel/variants/__init__.py b/pm4py/objects/ocel/exporter/jsonocel/variants/__init__.py index 08d3fe872..c181e4310 100644 --- a/pm4py/objects/ocel/exporter/jsonocel/variants/__init__.py +++ b/pm4py/objects/ocel/exporter/jsonocel/variants/__init__.py @@ -1 +1 @@ -from pm4py.objects.ocel.exporter.jsonocel.variants import classic +from pm4py.objects.ocel.exporter.jsonocel.variants import classic, ocel20 diff --git a/pm4py/objects/ocel/exporter/jsonocel/variants/ocel20.py b/pm4py/objects/ocel/exporter/jsonocel/variants/ocel20.py new file mode 100644 index 000000000..8a4b3b871 --- /dev/null +++ b/pm4py/objects/ocel/exporter/jsonocel/variants/ocel20.py @@ -0,0 +1,117 @@ +import json +from enum import Enum +from typing import Optional, Dict, Any + +import pandas as pd + +from pm4py.objects.ocel import constants +from pm4py.objects.ocel.obj import OCEL +from pm4py.util import exec_utils, constants as pm4_constants +from pm4py.objects.ocel.util import ocel_consistency +from pm4py.objects.ocel.exporter.jsonocel.variants import classic +from pm4py.objects.ocel.util import attributes_per_type + + +class Parameters(Enum): + EVENT_ID = constants.PARAM_EVENT_ID + OBJECT_ID = constants.PARAM_OBJECT_ID + OBJECT_TYPE = constants.PARAM_OBJECT_TYPE + EVENT_ACTIVITY = constants.PARAM_EVENT_ACTIVITY + EVENT_TIMESTAMP = constants.PARAM_EVENT_TIMESTAMP + ENCODING = "encoding" + + +def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = None): + """ + Exports an object-centric event log (OCEL 2.0) in a JSONOCEL 2.0 file, using the classic JSON dump + + Parameters + ------------------ + ocel + Object-centric event log + target_path + Destination path + parameters + Parameters of the algorithm, including: + - Parameters.EVENT_ID => the event ID column + - Parameters.OBJECT_ID => the object ID column + - Parameters.OBJECT_TYPE => the object type column + """ + if parameters is None: + parameters = {} + + event_id = exec_utils.get_param_value(Parameters.EVENT_ID, parameters, ocel.event_id_column) + object_id = exec_utils.get_param_value(Parameters.OBJECT_ID, parameters, ocel.object_id_column) + object_type = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, ocel.object_type_column) + event_activity = exec_utils.get_param_value(Parameters.EVENT_ACTIVITY, parameters, ocel.event_activity) + event_timestamp = exec_utils.get_param_value(Parameters.EVENT_TIMESTAMP, parameters, ocel.event_timestamp) + + encoding = exec_utils.get_param_value(Parameters.ENCODING, parameters, pm4_constants.DEFAULT_ENCODING) + + ocel = ocel_consistency.apply(ocel, parameters=parameters) + + base_object = classic.get_base_json_object(ocel, parameters=parameters) + + ets, ots = attributes_per_type.get(ocel, parameters=parameters) + + base_object[constants.OCEL_EVTYPES_KEY] = {} + for et in ets: + base_object[constants.OCEL_EVTYPES_KEY][et] = {} + et_atts = ets[et] + for k, v in et_atts.items(): + this_type = "string" + if "date" in v or "time" in v: + this_type = "date" + elif "float" in v or "double" in v: + this_type = "float" + base_object[constants.OCEL_EVTYPES_KEY][et][k] = this_type + + base_object[constants.OCEL_OBJTYPES_KEY] = {} + for ot in ots: + base_object[constants.OCEL_OBJTYPES_KEY][ot] = {} + ot_atts = ots[ot] + for k, v in ot_atts.items(): + this_type = "string" + if "date" in v or "time" in v: + this_type = "date" + elif "float" in v or "double" in v: + this_type = "float" + base_object[constants.OCEL_OBJTYPES_KEY][ot][k] = this_type + + base_object[constants.OCEL_OBJCHANGES_KEY] = [] + if len(ocel.object_changes) > 0: + object_changes = ocel.object_changes.to_dict("records") + for i in range(len(object_changes)): + object_changes[i][event_timestamp] = object_changes[i][event_timestamp].isoformat() + + base_object[constants.OCEL_OBJCHANGES_KEY] = object_changes + + e2o_list = ocel.relations[[event_id, object_id, constants.DEFAULT_QUALIFIER]].to_dict("records") + eids = set() + + for elem in e2o_list: + eid = elem[event_id] + oid = elem[object_id] + qualifier = elem[constants.DEFAULT_QUALIFIER] + + if eid not in eids: + base_object[constants.OCEL_EVENTS_KEY][eid][constants.OCEL_TYPED_OMAP_KEY] = [] + eids.add(eid) + + base_object[constants.OCEL_EVENTS_KEY][eid][constants.OCEL_TYPED_OMAP_KEY].append({object_id: oid, constants.DEFAULT_QUALIFIER: qualifier}) + + o2o_list = ocel.o2o.to_dict("records") + oids = set() + + for elem in o2o_list: + oid = elem[object_id] + oid2 = elem[object_id+"_2"] + qualifier = elem[constants.DEFAULT_QUALIFIER] + + if oid not in oids: + base_object[constants.OCEL_OBJECTS_KEY][oid][constants.OCEL_O2O_KEY] = [] + oids.add(oid) + + base_object[constants.OCEL_OBJECTS_KEY][oid][constants.OCEL_O2O_KEY].append({object_id: oid2, constants.DEFAULT_QUALIFIER: qualifier}) + + json.dump(base_object, open(target_path, "w", encoding=encoding), indent=2) diff --git a/pm4py/objects/ocel/importer/jsonocel/variants/classic.py b/pm4py/objects/ocel/importer/jsonocel/variants/classic.py index 79ad245c9..f76c0f475 100644 --- a/pm4py/objects/ocel/importer/jsonocel/variants/classic.py +++ b/pm4py/objects/ocel/importer/jsonocel/variants/classic.py @@ -25,6 +25,8 @@ def get_base_ocel(json_obj: Any, parameters: Optional[Dict[Any, Any]] = None): events = [] relations = [] objects = [] + o2o = [] + object_changes = [] event_id = exec_utils.get_param_value(Parameters.EVENT_ID, parameters, constants.DEFAULT_EVENT_ID) event_activity = exec_utils.get_param_value(Parameters.EVENT_ACTIVITY, parameters, constants.DEFAULT_EVENT_ACTIVITY) @@ -44,6 +46,12 @@ def get_base_ocel(json_obj: Any, parameters: Optional[Dict[Any, Any]] = None): dct = {object_id: obj_id, object_type: obj_type} for k, v in obj[constants.OCEL_OVMAP_KEY].items(): dct[k] = v + if constants.OCEL_O2O_KEY in obj: + this_rel_objs = obj[constants.OCEL_O2O_KEY] + for newel in this_rel_objs: + target_id = newel[object_id] + qualifier = newel[constants.DEFAULT_QUALIFIER] + o2o.append({object_id: obj_id, object_id+"_2": target_id, constants.DEFAULT_QUALIFIER: qualifier}) objects.append(dct) for ev_id in json_obj[constants.OCEL_EVENTS_KEY]: @@ -64,6 +72,9 @@ def get_base_ocel(json_obj: Any, parameters: Optional[Dict[Any, Any]] = None): relations.append(this_rel[obj]) events.append(dct) + if constants.OCEL_OBJCHANGES_KEY in json_obj: + object_changes = json_obj[constants.OCEL_OBJCHANGES_KEY] + events = pd.DataFrame(events) objects = pd.DataFrame(objects) relations = pd.DataFrame(relations) @@ -82,7 +93,15 @@ def get_base_ocel(json_obj: Any, parameters: Optional[Dict[Any, Any]] = None): globals[constants.OCEL_GLOBAL_EVENT] = json_obj[constants.OCEL_GLOBAL_EVENT] globals[constants.OCEL_GLOBAL_OBJECT] = json_obj[constants.OCEL_GLOBAL_OBJECT] - log = OCEL(events=events, objects=objects, relations=relations, globals=globals, parameters=parameters) + o2o = pd.DataFrame(o2o) if o2o else None + object_changes = pd.DataFrame(object_changes) if object_changes else None + if object_changes is not None and len(object_changes) > 0: + object_changes[event_timestamp] = pd.to_datetime(object_changes[event_timestamp]) + obj_id_map = objects[[object_id, object_type]].to_dict("records") + obj_id_map = {x[object_id]: x[object_type] for x in obj_id_map} + object_changes[object_type] = object_changes[object_id].map(obj_id_map) + + log = OCEL(events=events, objects=objects, relations=relations, o2o=o2o, object_changes=object_changes, globals=globals, parameters=parameters) return log diff --git a/pm4py/read.py b/pm4py/read.py index f980df341..a99cd4498 100644 --- a/pm4py/read.py +++ b/pm4py/read.py @@ -284,6 +284,8 @@ def read_ocel2(file_path: str) -> OCEL: return read_ocel2_sqlite(file_path) elif file_path.lower().endswith("xml") or file_path.lower().endswith("xmlocel"): return read_ocel2_xml(file_path) + elif file_path.lower().endswith("jsonocel"): + return read_ocel_json(file_path) def read_ocel2_sqlite(file_path: str) -> OCEL: diff --git a/pm4py/write.py b/pm4py/write.py index c6421d7b5..8748f5f9c 100644 --- a/pm4py/write.py +++ b/pm4py/write.py @@ -218,7 +218,11 @@ def write_ocel_json(ocel: OCEL, file_path: str): file_path = file_path + ".jsonocel" from pm4py.objects.ocel.exporter.jsonocel import exporter as jsonocel_exporter - return jsonocel_exporter.apply(ocel, file_path, variant=jsonocel_exporter.Variants.CLASSIC) + + is_ocel20 = ocel.is_ocel20() + variant = jsonocel_exporter.Variants.OCEL20 if is_ocel20 else jsonocel_exporter.Variants.CLASSIC + + return jsonocel_exporter.apply(ocel, file_path, variant=variant) def write_ocel_xml(ocel: OCEL, file_path: str): @@ -282,6 +286,8 @@ def write_ocel2(ocel: OCEL, file_path: str): return write_ocel2_sqlite(ocel, file_path) elif file_path.lower().endswith("xml") or file_path.lower().endswith("xmlocel"): return write_ocel2_xml(ocel, file_path) + elif file_path.lower().endswith("jsonocel"): + return write_ocel_json(ocel, file_path) def write_ocel2_sqlite(ocel: OCEL, file_path: str): diff --git a/tests/input_data/ocel/ocel20_example.jsonocel b/tests/input_data/ocel/ocel20_example.jsonocel new file mode 100644 index 000000000..564cb1727 --- /dev/null +++ b/tests/input_data/ocel/ocel20_example.jsonocel @@ -0,0 +1,430 @@ +{ + "ocel:global-event": {}, + "ocel:global-object": {}, + "ocel:global-log": { + "ocel:object-types": [ + "Invoice", + "Payment", + "Purchase Order", + "Purchase Requisition" + ], + "ocel:attribute-names": [ + "invoice_block_rem", + "invoice_blocker", + "invoice_inserter", + "is_blocked", + "payment_inserter", + "po_creator", + "po_editor", + "po_product", + "po_quantity", + "pr_approver", + "pr_creator", + "pr_product", + "pr_quantity" + ], + "ocel:version": "1.0", + "ocel:ordering": "timestamp" + }, + "ocel:events": { + "e1": { + "ocel:timestamp": "2022-01-09T14:00:00+00:00", + "ocel:activity": "Create Purchase Requisition", + "ocel:vmap": { + "pr_creator": "Mike" + }, + "ocel:omap": [ + "PR1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PR1", + "ocel:qualifier": "Regular placement of PR" + } + ] + }, + "e2": { + "ocel:timestamp": "2022-01-09T15:30:00+00:00", + "ocel:activity": "Approve Purchase Requisition", + "ocel:vmap": { + "pr_approver": "Tania" + }, + "ocel:omap": [ + "PR1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PR1", + "ocel:qualifier": "Regular approval of PR" + } + ] + }, + "e3": { + "ocel:timestamp": "2022-01-10T08:15:00+00:00", + "ocel:activity": "Create Purchase Order", + "ocel:vmap": { + "po_creator": "Mike" + }, + "ocel:omap": [ + "PR1", + "PO1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PR1", + "ocel:qualifier": "Created order from PR" + }, + { + "ocel:oid": "PO1", + "ocel:qualifier": "Created order with identifier" + } + ] + }, + "e4": { + "ocel:timestamp": "2022-01-13T11:00:00+00:00", + "ocel:activity": "Change PO Quantity", + "ocel:vmap": { + "po_editor": "Mike" + }, + "ocel:omap": [ + "PO1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PO1", + "ocel:qualifier": "Change of quantity" + } + ] + }, + "e5": { + "ocel:timestamp": "2022-01-14T11:00:00+00:00", + "ocel:activity": "Insert Invoice", + "ocel:vmap": { + "invoice_inserter": "Luke" + }, + "ocel:omap": [ + "PO1", + "R1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PO1", + "ocel:qualifier": "Invoice created starting from the PO" + }, + { + "ocel:oid": "R1", + "ocel:qualifier": "Invoice created with identifier" + } + ] + }, + "e6": { + "ocel:timestamp": "2022-01-16T10:00:00+00:00", + "ocel:activity": "Insert Invoice", + "ocel:vmap": { + "invoice_inserter": "Luke" + }, + "ocel:omap": [ + "PO1", + "R2" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "PO1", + "ocel:qualifier": "Invoice created starting from the PO" + }, + { + "ocel:oid": "R2", + "ocel:qualifier": "Invoice created with identifier" + } + ] + }, + "e7": { + "ocel:timestamp": "2022-01-30T22:00:00+00:00", + "ocel:activity": "Insert Payment", + "ocel:vmap": { + "payment_inserter": "Robot" + }, + "ocel:omap": [ + "R1", + "P1" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R1", + "ocel:qualifier": "Payment for the invoice" + }, + { + "ocel:oid": "P1", + "ocel:qualifier": "Payment inserted with identifier" + } + ] + }, + "e8": { + "ocel:timestamp": "2022-01-31T21:00:00+00:00", + "ocel:activity": "Insert Payment", + "ocel:vmap": { + "payment_inserter": "Robot" + }, + "ocel:omap": [ + "R2", + "P2" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R2", + "ocel:qualifier": "Payment for the invoice" + }, + { + "ocel:oid": "P2", + "ocel:qualifier": "Payment created with identifier" + } + ] + }, + "e9": { + "ocel:timestamp": "2022-02-02T08:00:00+00:00", + "ocel:activity": "Insert Invoice", + "ocel:vmap": { + "invoice_inserter": "Mario" + }, + "ocel:omap": [ + "R3" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Invoice created with identifier" + } + ] + }, + "e10": { + "ocel:timestamp": "2022-02-02T16:00:00+00:00", + "ocel:activity": "Create Purchase Order", + "ocel:vmap": { + "po_creator": "Mario" + }, + "ocel:omap": [ + "R3", + "PO2" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Purchase order created with maverick buying from" + }, + { + "ocel:oid": "PO2", + "ocel:qualifier": "Purhcase order created with identifier" + } + ] + }, + "e11": { + "ocel:timestamp": "2022-02-03T06:30:00+00:00", + "ocel:activity": "Set Payment Block", + "ocel:vmap": { + "invoice_blocker": "Mario" + }, + "ocel:omap": [ + "R3" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Payment block due to unethical maverick buying" + } + ] + }, + "e12": { + "ocel:timestamp": "2022-02-03T22:30:00+00:00", + "ocel:activity": "Remove Payment Block", + "ocel:vmap": { + "invoice_block_rem": "Mario" + }, + "ocel:omap": [ + "R3" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Payment block removed ..." + } + ] + }, + "e13": { + "ocel:timestamp": "2022-02-28T22:00:00+00:00", + "ocel:activity": "Insert Payment", + "ocel:vmap": { + "payment_inserter": "Robot" + }, + "ocel:omap": [ + "R3", + "P3" + ], + "ocel:typedOmap": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Payment for the invoice" + }, + { + "ocel:oid": "P3", + "ocel:qualifier": "Payment inserted with identifier" + } + ] + } + }, + "ocel:objects": { + "R1": { + "ocel:type": "Invoice", + "ocel:ovmap": { + "is_blocked": "No" + }, + "ocel:o2o": [ + { + "ocel:oid": "P1", + "ocel:qualifier": "Payment from invoice" + } + ] + }, + "R2": { + "ocel:type": "Invoice", + "ocel:ovmap": { + "is_blocked": "No" + }, + "ocel:o2o": [ + { + "ocel:oid": "P2", + "ocel:qualifier": "Payment from invoice" + } + ] + }, + "R3": { + "ocel:type": "Invoice", + "ocel:ovmap": { + "is_blocked": "No" + }, + "ocel:o2o": [ + { + "ocel:oid": "P3", + "ocel:qualifier": "Payment from invoice" + } + ] + }, + "P1": { + "ocel:type": "Payment", + "ocel:ovmap": {} + }, + "P2": { + "ocel:type": "Payment", + "ocel:ovmap": {} + }, + "P3": { + "ocel:type": "Payment", + "ocel:ovmap": {} + }, + "PO1": { + "ocel:type": "Purchase Order", + "ocel:ovmap": { + "po_product": "Cows", + "po_quantity": "500" + }, + "ocel:o2o": [ + { + "ocel:oid": "R1", + "ocel:qualifier": "Invoice from PO" + }, + { + "ocel:oid": "R2", + "ocel:qualifier": "Invoice from PO" + } + ] + }, + "PO2": { + "ocel:type": "Purchase Order", + "ocel:ovmap": { + "po_product": "Notebooks", + "po_quantity": "1" + }, + "ocel:o2o": [ + { + "ocel:oid": "R3", + "ocel:qualifier": "Maverick buying" + } + ] + }, + "PR1": { + "ocel:type": "Purchase Requisition", + "ocel:ovmap": { + "pr_product": "Cows", + "pr_quantity": "500" + }, + "ocel:o2o": [ + { + "ocel:oid": "PO1", + "ocel:qualifier": "PO from PR" + } + ] + } + }, + "ocel:eventTypes": { + "Approve Purchase Requisition": { + "pr_approver": "string" + }, + "Change PO Quantity": { + "po_editor": "string" + }, + "Create Purchase Order": { + "po_creator": "string" + }, + "Create Purchase Requisition": { + "pr_creator": "string" + }, + "Insert Invoice": { + "invoice_inserter": "string" + }, + "Insert Payment": { + "payment_inserter": "string" + }, + "Remove Payment Block": { + "invoice_block_rem": "string" + }, + "Set Payment Block": { + "invoice_blocker": "string" + } + }, + "ocel:objectTypes": { + "Invoice": { + "is_blocked": "string" + }, + "Payment": {}, + "Purchase Order": { + "po_product": "string", + "po_quantity": "string" + }, + "Purchase Requisition": { + "pr_product": "string", + "pr_quantity": "string" + } + }, + "ocel:objectChanges": [ + { + "ocel:oid": "R3", + "ocel:name": "is_blocked", + "ocel:timestamp": "2022-02-03T06:30:00+00:00", + "ocel:value": "Yes", + "ocel:type": "Invoice" + }, + { + "ocel:oid": "R3", + "ocel:name": "is_blocked", + "ocel:timestamp": "2022-02-03T22:30:00+00:00", + "ocel:value": "No", + "ocel:type": "Invoice" + }, + { + "ocel:oid": "PO1", + "ocel:name": "po_quantity", + "ocel:timestamp": "2022-01-13T11:00:00+00:00", + "ocel:value": "600", + "ocel:type": "Purchase Order" + } + ] +} \ No newline at end of file diff --git a/tests/simplified_interface.py b/tests/simplified_interface.py index 5acaff2a8..cef2ec06d 100644 --- a/tests/simplified_interface.py +++ b/tests/simplified_interface.py @@ -1015,6 +1015,16 @@ def test_ocel_add_index_based_timedelta(self): ocel = pm4py.read_ocel("input_data/ocel/example_log.jsonocel") filtered_ocel = pm4py.ocel_add_index_based_timedelta(ocel) + def test_ocel2_xml(self): + ocel = pm4py.read_ocel2("input_data/ocel/ocel20_example.xmlocel") + pm4py.write_ocel2(ocel, "test_output_data/ocel20_example.xmlocel") + os.remove("test_output_data/ocel20_example.xmlocel") + + def test_ocel2_sqlite(self): + ocel = pm4py.read_ocel2("input_data/ocel/ocel20_example.sqlite") + pm4py.write_ocel2(ocel, "test_output_data/ocel20_example.sqlite") + os.remove("test_output_data/ocel20_example.sqlite") + if __name__ == "__main__": unittest.main()