diff --git a/pm4py/objects/ocel/constants.py b/pm4py/objects/ocel/constants.py index 33853b605..a917c3814 100644 --- a/pm4py/objects/ocel/constants.py +++ b/pm4py/objects/ocel/constants.py @@ -21,8 +21,13 @@ OCEL_OBJECTS_KEY = "ocel:objects" OCEL_ID_KEY = "ocel:id" OCEL_OMAP_KEY = "ocel:omap" +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/classic.py b/pm4py/objects/ocel/exporter/jsonocel/variants/classic.py index 2907f6659..0f0a37355 100644 --- a/pm4py/objects/ocel/exporter/jsonocel/variants/classic.py +++ b/pm4py/objects/ocel/exporter/jsonocel/variants/classic.py @@ -20,31 +20,13 @@ class Parameters(Enum): ENCODING = "encoding" -def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = None): - """ - Exports an object-centric event log in a JSONOCEL 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 - """ +def get_base_json_object(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None): 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) - encoding = exec_utils.get_param_value(Parameters.ENCODING, parameters, pm4_constants.DEFAULT_ENCODING) - - ocel = ocel_consistency.apply(ocel, parameters=parameters) all_object_types = list(ocel.objects[object_type].unique()) all_attribute_names = attributes_names.get_attribute_names(ocel, parameters=parameters) @@ -56,16 +38,16 @@ def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = N events_items, objects_items = clean_dataframes.get_dataframes_from_ocel(ocel, parameters=parameters) - result = {} - result[constants.OCEL_GLOBAL_EVENT] = global_event_items - result[constants.OCEL_GLOBAL_OBJECT] = global_object_items - result[constants.OCEL_GLOBAL_LOG] = {} - result[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_OBJECT_TYPES] = all_object_types - result[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_ATTRIBUTE_NAMES] = all_attribute_names - result[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_VERSION] = constants.CURRENT_VERSION - result[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_ORDERING] = constants.DEFAULT_ORDERING - result[constants.OCEL_EVENTS_KEY] = {} - result[constants.OCEL_OBJECTS_KEY] = {} + base_object = {} + base_object[constants.OCEL_GLOBAL_EVENT] = global_event_items + base_object[constants.OCEL_GLOBAL_OBJECT] = global_object_items + base_object[constants.OCEL_GLOBAL_LOG] = {} + base_object[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_OBJECT_TYPES] = all_object_types + base_object[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_ATTRIBUTE_NAMES] = all_attribute_names + base_object[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_VERSION] = constants.CURRENT_VERSION + base_object[constants.OCEL_GLOBAL_LOG][constants.OCEL_GLOBAL_LOG_ORDERING] = constants.DEFAULT_ORDERING + base_object[constants.OCEL_EVENTS_KEY] = {} + base_object[constants.OCEL_OBJECTS_KEY] = {} events_items = events_items.to_dict("records") i = 0 @@ -78,7 +60,7 @@ def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = N event = {k: v for k, v in event.items() if k.startswith(constants.OCEL_PREFIX)} event[constants.OCEL_VMAP_KEY] = vmap event[constants.OCEL_OMAP_KEY] = rel_objs[eid] - result[constants.OCEL_EVENTS_KEY][eid] = event + base_object[constants.OCEL_EVENTS_KEY][eid] = event i = i + 1 del events_items @@ -90,8 +72,36 @@ def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = N del object[object_id] ovmap = {k: v for k, v in object.items() if pd.notnull(v) and not k.startswith(constants.OCEL_PREFIX)} object = {object_type: object[object_type], constants.OCEL_OVMAP_KEY: ovmap} - result[constants.OCEL_OBJECTS_KEY][oid] = object + base_object[constants.OCEL_OBJECTS_KEY][oid] = object i = i + 1 del objects_items - json.dump(result, open(target_path, "w", encoding=encoding), indent=2) + return base_object + + +def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = None): + """ + Exports an object-centric event log in a JSONOCEL 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 = {} + + encoding = exec_utils.get_param_value(Parameters.ENCODING, parameters, pm4_constants.DEFAULT_ENCODING) + + ocel = ocel_consistency.apply(ocel, parameters=parameters) + + base_object = get_base_json_object(ocel, parameters=parameters) + + json.dump(base_object, open(target_path, "w", encoding=encoding), indent=2) 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/exporter/xmlocel/variants/ocel20.py b/pm4py/objects/ocel/exporter/xmlocel/variants/ocel20.py index 9a9b96be0..a6880f416 100644 --- a/pm4py/objects/ocel/exporter/xmlocel/variants/ocel20.py +++ b/pm4py/objects/ocel/exporter/xmlocel/variants/ocel20.py @@ -5,12 +5,10 @@ from lxml import etree from pm4py.objects.ocel import constants -from pm4py.objects.ocel.exporter.util import clean_dataframes from pm4py.objects.ocel.obj import OCEL -from pm4py.objects.ocel.util import attributes_names -from pm4py.objects.ocel.util import related_objects from pm4py.util import exec_utils, constants as pm4_constants from pm4py.objects.ocel.util import ocel_consistency +from pm4py.objects.ocel.util import attributes_per_type class Parameters(Enum): @@ -42,22 +40,9 @@ def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = N ocel = ocel_consistency.apply(ocel, parameters=parameters) - ets = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.events.groupby(event_activity_column)} - ots = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.objects.groupby(object_type_column)} - ots2 = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.object_changes.groupby(object_type_column)} - - for k in ots2: - if k not in ots: - ots[k] = ots2[k] - else: - for x in ots2[k]: - if x not in ots[k]: - ots[k][x] = ots2[k][x] - objects0 = ocel.objects.to_dict("records") events0 = ocel.events.to_dict("records") object_changes0 = ocel.object_changes.to_dict("records") - o2o_list = ocel.o2o.to_dict("records") o2o_dict = ocel.o2o.groupby(object_id_column).agg(list).to_dict("tight") o2o_dict = {o2o_dict["index"][i]: o2o_dict["data"][i] for i in range(len(o2o_dict["index"]))} @@ -100,6 +85,8 @@ def apply(ocel: OCEL, target_path: str, parameters: Optional[Dict[Any, Any]] = N object_changes2[oid][chng_field].append((chng_value, chng_time.isoformat())) object_changes3[oid].append((chng_field, chng_value, chng_time.isoformat())) + ets, ots = attributes_per_type.get(ocel, parameters=parameters) + root = etree.Element("log") object_types = etree.SubElement(root, "object-types") for ot in ots: diff --git a/pm4py/objects/ocel/importer/jsonocel/variants/classic.py b/pm4py/objects/ocel/importer/jsonocel/variants/classic.py index 7101ed867..f76c0f475 100644 --- a/pm4py/objects/ocel/importer/jsonocel/variants/classic.py +++ b/pm4py/objects/ocel/importer/jsonocel/variants/classic.py @@ -21,38 +21,12 @@ class Parameters(Enum): ENCODING = "encoding" -def apply(file_path: str, parameters: Optional[Dict[Any, Any]] = None) -> OCEL: - """ - Imports an object-centric event log from a JSON-OCEL file, using the default JSON backend of Python - - Parameters - ----------------- - file_path - Path to the JSON-OCEL file - parameters - Parameters of the algorithm, including: - - Parameters.EVENT_ID - - Parameters.EVENT_ACTIVITY - - Parameters.EVENT_TIMESTAMP - - Parameters.OBJECT_ID - - Parameters.OBJECT_TYPE - - Parameters.INTERNAL_INDEX - - Returns - ------------------ - ocel - Object-centric event log - """ - if parameters is None: - parameters = {} - - encoding = exec_utils.get_param_value(Parameters.ENCODING, parameters, pm4_constants.DEFAULT_ENCODING) - - ocel = json.load(open(file_path, "r", encoding=encoding)) - +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) @@ -65,27 +39,42 @@ def apply(file_path: str, parameters: Optional[Dict[Any, Any]] = None) -> OCEL: parser = dt_parsing.parser.get() types_dict = {} - for obj_id in ocel[constants.OCEL_OBJECTS_KEY]: - obj = ocel[constants.OCEL_OBJECTS_KEY][obj_id] + for obj_id in json_obj[constants.OCEL_OBJECTS_KEY]: + obj = json_obj[constants.OCEL_OBJECTS_KEY][obj_id] obj_type = obj[object_type] types_dict[obj_id] = obj_type 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 ocel[constants.OCEL_EVENTS_KEY]: - ev = ocel[constants.OCEL_EVENTS_KEY][ev_id] + for ev_id in json_obj[constants.OCEL_EVENTS_KEY]: + ev = json_obj[constants.OCEL_EVENTS_KEY][ev_id] dct = {event_id: ev_id, event_timestamp: parser.apply(ev[event_timestamp]), event_activity: ev[event_activity]} for k, v in ev[constants.OCEL_VMAP_KEY].items(): dct[k] = v + this_rel = {} for obj in ev[constants.OCEL_OMAP_KEY]: - relations.append({event_id: ev_id, event_activity: ev[event_activity], + this_rel[obj] = {event_id: ev_id, event_activity: ev[event_activity], event_timestamp: parser.apply(ev[event_timestamp]), object_id: obj, - object_type: types_dict[obj]}) + object_type: types_dict[obj]} + if constants.OCEL_TYPED_OMAP_KEY in ev: + for element in ev[constants.OCEL_TYPED_OMAP_KEY]: + this_rel[element[object_id]][constants.DEFAULT_QUALIFIER] = element[constants.DEFAULT_QUALIFIER] + for obj in this_rel: + 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) @@ -100,12 +89,55 @@ def apply(file_path: str, parameters: Optional[Dict[Any, Any]] = None) -> OCEL: del relations[internal_index] globals = {} - globals[constants.OCEL_GLOBAL_LOG] = ocel[constants.OCEL_GLOBAL_LOG] - globals[constants.OCEL_GLOBAL_EVENT] = ocel[constants.OCEL_GLOBAL_EVENT] - globals[constants.OCEL_GLOBAL_OBJECT] = ocel[constants.OCEL_GLOBAL_OBJECT] + globals[constants.OCEL_GLOBAL_LOG] = json_obj[constants.OCEL_GLOBAL_LOG] + globals[constants.OCEL_GLOBAL_EVENT] = json_obj[constants.OCEL_GLOBAL_EVENT] + globals[constants.OCEL_GLOBAL_OBJECT] = json_obj[constants.OCEL_GLOBAL_OBJECT] + + 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 + + +def apply(file_path: str, parameters: Optional[Dict[Any, Any]] = None) -> OCEL: + """ + Imports an object-centric event log from a JSON-OCEL file, using the default JSON backend of Python + + Parameters + ----------------- + file_path + Path to the JSON-OCEL file + parameters + Parameters of the algorithm, including: + - Parameters.EVENT_ID + - Parameters.EVENT_ACTIVITY + - Parameters.EVENT_TIMESTAMP + - Parameters.OBJECT_ID + - Parameters.OBJECT_TYPE + - Parameters.INTERNAL_INDEX + + Returns + ------------------ + ocel + Object-centric event log + """ + if parameters is None: + parameters = {} + + encoding = exec_utils.get_param_value(Parameters.ENCODING, parameters, pm4_constants.DEFAULT_ENCODING) + + json_obj = json.load(open(file_path, "r", encoding=encoding)) + + log = get_base_ocel(json_obj, parameters=parameters) - ocel = OCEL(events=events, objects=objects, relations=relations, globals=globals, parameters=parameters) - ocel = ocel_consistency.apply(ocel, parameters=parameters) - ocel = filtering_utils.propagate_relations_filtering(ocel, parameters=parameters) + log = ocel_consistency.apply(log, parameters=parameters) + log = filtering_utils.propagate_relations_filtering(log, parameters=parameters) - return ocel + return log diff --git a/pm4py/objects/ocel/obj.py b/pm4py/objects/ocel/obj.py index ec4d32c3a..33d5c9667 100644 --- a/pm4py/objects/ocel/obj.py +++ b/pm4py/objects/ocel/obj.py @@ -3,6 +3,7 @@ from pm4py.objects.ocel import constants from pm4py.util import exec_utils import pandas as pd +import numpy as np from copy import copy, deepcopy @@ -98,6 +99,22 @@ def get_summary(self) -> str: "Please use .get_extended_table() to get a dataframe representation of the events related to the objects.") return "".join(ret) + def is_ocel20(self): + unique_qualifiers = [] + if self.qualifier in self.relations.columns: + unique_qualifiers = [x for x in self.relations[self.qualifier].unique() if not self.__check_is_nan(x)] + + return len(self.o2o) > 0 or len(self.object_changes) > 0 or len(unique_qualifiers) > 0 + + def __check_is_nan(self, x): + try: + if x is None: + return True + if np.isnan(x): + return True + except: + return False + def __str__(self): return str(self.get_summary()) diff --git a/pm4py/objects/ocel/util/attributes_per_type.py b/pm4py/objects/ocel/util/attributes_per_type.py new file mode 100644 index 000000000..cc2b65b0a --- /dev/null +++ b/pm4py/objects/ocel/util/attributes_per_type.py @@ -0,0 +1,32 @@ +from pm4py.objects.ocel.obj import OCEL +from typing import Optional, Dict, Any +from pm4py.objects.ocel import constants +from enum import Enum +from pm4py.util import exec_utils + + +class Parameters(Enum): + EVENT_ACTIVITY = constants.PARAM_EVENT_ACTIVITY + OBJECT_TYPE = constants.PARAM_OBJECT_TYPE + + +def get(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None): + if parameters is None: + parameters = {} + + event_activity_column = exec_utils.get_param_value(Parameters.EVENT_ACTIVITY, parameters, ocel.event_activity) + object_type_column = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, ocel.object_type_column) + + ets = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.events.groupby(event_activity_column)} + ots = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.objects.groupby(object_type_column)} + ots2 = {k: {x: str(v[x].dtype) for x in v.dropna(axis="columns", how="all").columns if not x.startswith("ocel:")} for k, v in ocel.object_changes.groupby(object_type_column)} + + for k in ots2: + if k not in ots: + ots[k] = ots2[k] + else: + for x in ots2[k]: + if x not in ots[k]: + ots[k][x] = ots2[k][x] + + return ets, ots diff --git a/pm4py/read.py b/pm4py/read.py index 83bc2af76..58b75f613 100644 --- a/pm4py/read.py +++ b/pm4py/read.py @@ -299,6 +299,8 @@ def read_ocel2(file_path: str, encoding: str = constants.DEFAULT_ENCODING) -> OC return read_ocel2_sqlite(file_path, encoding=encoding) elif file_path.lower().endswith("xml") or file_path.lower().endswith("xmlocel"): return read_ocel2_xml(file_path, encoding=encoding) + elif file_path.lower().endswith("jsonocel"): + return read_ocel_json(file_path, encoding=encoding) def read_ocel2_sqlite(file_path: str, encoding: str = constants.DEFAULT_ENCODING) -> OCEL: diff --git a/pm4py/write.py b/pm4py/write.py index 9139e8b16..f79cbe64f 100644 --- a/pm4py/write.py +++ b/pm4py/write.py @@ -228,7 +228,11 @@ def write_ocel_json(ocel: OCEL, file_path: str, encoding: str = constants.DEFAUL 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, parameters={"encoding": encoding}) + + 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, parameters={"encoding": encoding}) def write_ocel_xml(ocel: OCEL, file_path: str, encoding: str = constants.DEFAULT_ENCODING): @@ -295,6 +299,8 @@ def write_ocel2(ocel: OCEL, file_path: str, encoding: str = constants.DEFAULT_EN return write_ocel2_sqlite(ocel, file_path, encoding=encoding) elif file_path.lower().endswith("xml") or file_path.lower().endswith("xmlocel"): return write_ocel2_xml(ocel, file_path, encoding=encoding) + elif file_path.lower().endswith("jsonocel"): + return write_ocel_json(ocel, file_path, encoding=encoding) def write_ocel2_sqlite(ocel: OCEL, file_path: str, encoding: str = constants.DEFAULT_ENCODING): 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()