diff --git a/pm4py/__init__.py b/pm4py/__init__.py
index fa0cc2b3a..4852e1207 100644
--- a/pm4py/__init__.py
+++ b/pm4py/__init__.py
@@ -201,6 +201,10 @@
ocel_o2o_enrichment,
ocel_e2o_lifecycle_enrichment,
cluster_equivalent_ocel,
+ ocel_drill_down,
+ ocel_roll_up,
+ ocel_unfold,
+ ocel_fold,
)
from pm4py.vis import (
view_petri_net,
diff --git a/pm4py/algo/transformation/ocel/olap/__init__.py b/pm4py/algo/transformation/ocel/olap/__init__.py
new file mode 100644
index 000000000..b7ca2075e
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap import drill_down, roll_up, unfold, fold
diff --git a/pm4py/algo/transformation/ocel/olap/drill_down/__init__.py b/pm4py/algo/transformation/ocel/olap/drill_down/__init__.py
new file mode 100644
index 000000000..7e2b131c3
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/drill_down/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.drill_down import algorithm
diff --git a/pm4py/algo/transformation/ocel/olap/drill_down/algorithm.py b/pm4py/algo/transformation/ocel/olap/drill_down/algorithm.py
new file mode 100644
index 000000000..9e8970888
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/drill_down/algorithm.py
@@ -0,0 +1,66 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from enum import Enum
+from typing import Any, Dict, Optional
+
+from pm4py.algo.transformation.ocel.olap.drill_down.variants import classic
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Variants(Enum):
+ CLASSIC = classic
+
+
+def apply(
+ ocel: OCEL,
+ variant=Variants.CLASSIC,
+ parameters: Optional[Dict[Any, Any]] = None,
+) -> OCEL:
+ """
+ Applies the drill-down OLAP operation on an object-centric event log,
+ splitting the target object type into tuple-style sub-types based on
+ the values of a given object attribute.
+
+ Reference: Khayatbashi, Miri, Jalali. "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations." CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ variant
+ Variant of the algorithm to be used, possible values:
+ - Variants.CLASSIC
+ parameters
+ Variant-specific parameters
+
+ Returns
+ --------------
+ new_ocel
+ A new object-centric event log with the drilled-down object type.
+ """
+ if parameters is None:
+ parameters = {}
+
+ return exec_utils.get_variant(variant).apply(ocel, parameters=parameters)
diff --git a/pm4py/algo/transformation/ocel/olap/drill_down/variants/__init__.py b/pm4py/algo/transformation/ocel/olap/drill_down/variants/__init__.py
new file mode 100644
index 000000000..6778740a4
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/drill_down/variants/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.drill_down.variants import classic
diff --git a/pm4py/algo/transformation/ocel/olap/drill_down/variants/classic.py b/pm4py/algo/transformation/ocel/olap/drill_down/variants/classic.py
new file mode 100644
index 000000000..35d19129e
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/drill_down/variants/classic.py
@@ -0,0 +1,129 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+import copy as copy_mod
+from enum import Enum
+from typing import Any, Callable, Dict, Optional
+
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Parameters(Enum):
+ OBJECT_TYPE = "object_type"
+ OBJECT_ATTRIBUTE = "object_attribute"
+ TUPLE_FORMAT = "tuple_format"
+
+
+def _default_format(parent: str, value: str) -> str:
+ return "(" + parent + ", " + value + ")"
+
+
+def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> OCEL:
+ """
+ Drill-down: splits a target object type into sub-types based on the
+ values of a given object attribute.
+
+ For every object whose type equals ``object_type`` and whose
+ ``object_attribute`` value is defined, the type is rewritten to
+ ``tuple_format(object_type, value)``. Objects whose attribute value is
+ undefined (NaN / empty) keep their original type.
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ parameters
+ - Parameters.OBJECT_TYPE: name of the object type to split (required)
+ - Parameters.OBJECT_ATTRIBUTE: name of the object attribute column
+ to split by (required; must be a column of ``ocel.objects``)
+ - Parameters.TUPLE_FORMAT: optional callable
+ ``(parent: str, value: str) -> str`` used to build the new type
+ name; defaults to ``"(parent, value)"``.
+
+ Notes
+ --------------
+ This implementation reads attribute values from ``ocel.objects`` only;
+ time-varying attribute values in ``ocel.object_changes`` are not used
+ to drive the split.
+
+ Returns a new OCEL; the input log is left untouched.
+ """
+ if parameters is None:
+ parameters = {}
+
+ ot = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, None)
+ oa = exec_utils.get_param_value(
+ Parameters.OBJECT_ATTRIBUTE, parameters, None
+ )
+ fmt: Callable[[str, str], str] = exec_utils.get_param_value(
+ Parameters.TUPLE_FORMAT, parameters, _default_format
+ )
+
+ if ot is None:
+ raise ValueError("drill_down requires Parameters.OBJECT_TYPE")
+ if oa is None:
+ raise ValueError("drill_down requires Parameters.OBJECT_ATTRIBUTE")
+
+ result = copy_mod.deepcopy(ocel)
+ type_col = result.object_type_column
+ oid_col = result.object_id_column
+
+ if ot not in set(result.objects[type_col].unique()):
+ raise ValueError(
+ "object type %r not found in ocel.objects[%r]" % (ot, type_col)
+ )
+ if oa not in result.objects.columns:
+ raise ValueError(
+ "object attribute %r is not a column of ocel.objects" % (oa,)
+ )
+
+ attr = result.objects[oa]
+ type_match = result.objects[type_col] == ot
+ attr_defined = attr.notna() & (attr.astype(str) != "")
+ mask = type_match & attr_defined
+
+ if mask.any():
+ new_types = [
+ fmt(ot, str(v)) for v in result.objects.loc[mask, oa].tolist()
+ ]
+ result.objects.loc[mask, type_col] = new_types
+
+ oid_to_type = dict(
+ zip(result.objects[oid_col], result.objects[type_col])
+ )
+ result.relations[type_col] = (
+ result.relations[oid_col]
+ .map(oid_to_type)
+ .fillna(result.relations[type_col])
+ )
+
+ if (
+ type_col in result.object_changes.columns
+ and len(result.object_changes) > 0
+ ):
+ result.object_changes[type_col] = (
+ result.object_changes[oid_col]
+ .map(oid_to_type)
+ .fillna(result.object_changes[type_col])
+ )
+
+ return result
diff --git a/pm4py/algo/transformation/ocel/olap/fold/__init__.py b/pm4py/algo/transformation/ocel/olap/fold/__init__.py
new file mode 100644
index 000000000..b803f2288
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/fold/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.fold import algorithm
diff --git a/pm4py/algo/transformation/ocel/olap/fold/algorithm.py b/pm4py/algo/transformation/ocel/olap/fold/algorithm.py
new file mode 100644
index 000000000..42b14cccb
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/fold/algorithm.py
@@ -0,0 +1,66 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from enum import Enum
+from typing import Any, Dict, Optional
+
+from pm4py.algo.transformation.ocel.olap.fold.variants import classic
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Variants(Enum):
+ CLASSIC = classic
+
+
+def apply(
+ ocel: OCEL,
+ variant=Variants.CLASSIC,
+ parameters: Optional[Dict[Any, Any]] = None,
+) -> OCEL:
+ """
+ Applies the fold OLAP operation on an object-centric event log,
+ collapsing a previously unfolded tuple-style event type back to its
+ parent event type. Inverse of unfold.
+
+ Reference: Khayatbashi, Miri, Jalali. "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations." CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ variant
+ Variant of the algorithm to be used, possible values:
+ - Variants.CLASSIC
+ parameters
+ Variant-specific parameters
+
+ Returns
+ --------------
+ new_ocel
+ A new object-centric event log with the folded event type.
+ """
+ if parameters is None:
+ parameters = {}
+
+ return exec_utils.get_variant(variant).apply(ocel, parameters=parameters)
diff --git a/pm4py/algo/transformation/ocel/olap/fold/variants/__init__.py b/pm4py/algo/transformation/ocel/olap/fold/variants/__init__.py
new file mode 100644
index 000000000..0af9bf1dc
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/fold/variants/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.fold.variants import classic
diff --git a/pm4py/algo/transformation/ocel/olap/fold/variants/classic.py b/pm4py/algo/transformation/ocel/olap/fold/variants/classic.py
new file mode 100644
index 000000000..4615010a1
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/fold/variants/classic.py
@@ -0,0 +1,86 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+import copy as copy_mod
+from enum import Enum
+from typing import Any, Callable, Dict, Optional
+
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Parameters(Enum):
+ EVENT_TYPE = "event_type"
+ OBJECT_TYPE = "object_type"
+ TUPLE_FORMAT = "tuple_format"
+
+
+def _default_format(parent: str, value: str) -> str:
+ return "(" + parent + ", " + value + ")"
+
+
+def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> OCEL:
+ """
+ Fold: the inverse of unfold. Replaces the unfolded activity
+ ``tuple_format(event_type, object_type)`` back with ``event_type`` in
+ both ``ocel.events`` and ``ocel.relations``.
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ parameters
+ - Parameters.EVENT_TYPE: the parent event type to merge back to
+ (required)
+ - Parameters.OBJECT_TYPE: the object type used during the matching
+ unfold (required)
+ - Parameters.TUPLE_FORMAT: optional callable matching unfold's
+ encoding; default ``"(event_type, object_type)"``.
+
+ Returns a new OCEL; the input log is left untouched.
+ """
+ if parameters is None:
+ parameters = {}
+
+ et = exec_utils.get_param_value(Parameters.EVENT_TYPE, parameters, None)
+ ot = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, None)
+ fmt: Callable[[str, str], str] = exec_utils.get_param_value(
+ Parameters.TUPLE_FORMAT, parameters, _default_format
+ )
+
+ if et is None:
+ raise ValueError("fold requires Parameters.EVENT_TYPE")
+ if ot is None:
+ raise ValueError("fold requires Parameters.OBJECT_TYPE")
+
+ result = copy_mod.deepcopy(ocel)
+ act_col = result.event_activity
+
+ folded_act = fmt(et, ot)
+
+ ev_mask = result.events[act_col] == folded_act
+ if ev_mask.any():
+ result.events.loc[ev_mask, act_col] = et
+ rel_mask = result.relations[act_col] == folded_act
+ if rel_mask.any():
+ result.relations.loc[rel_mask, act_col] = et
+
+ return result
diff --git a/pm4py/algo/transformation/ocel/olap/roll_up/__init__.py b/pm4py/algo/transformation/ocel/olap/roll_up/__init__.py
new file mode 100644
index 000000000..2a3feccf3
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/roll_up/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.roll_up import algorithm
diff --git a/pm4py/algo/transformation/ocel/olap/roll_up/algorithm.py b/pm4py/algo/transformation/ocel/olap/roll_up/algorithm.py
new file mode 100644
index 000000000..c92105e31
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/roll_up/algorithm.py
@@ -0,0 +1,66 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from enum import Enum
+from typing import Any, Dict, Optional
+
+from pm4py.algo.transformation.ocel.olap.roll_up.variants import classic
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Variants(Enum):
+ CLASSIC = classic
+
+
+def apply(
+ ocel: OCEL,
+ variant=Variants.CLASSIC,
+ parameters: Optional[Dict[Any, Any]] = None,
+) -> OCEL:
+ """
+ Applies the roll-up OLAP operation on an object-centric event log,
+ collapsing tuple-style sub-types of a given parent object type back
+ to the parent type. Inverse of drill-down.
+
+ Reference: Khayatbashi, Miri, Jalali. "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations." CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ variant
+ Variant of the algorithm to be used, possible values:
+ - Variants.CLASSIC
+ parameters
+ Variant-specific parameters
+
+ Returns
+ --------------
+ new_ocel
+ A new object-centric event log with the rolled-up object type.
+ """
+ if parameters is None:
+ parameters = {}
+
+ return exec_utils.get_variant(variant).apply(ocel, parameters=parameters)
diff --git a/pm4py/algo/transformation/ocel/olap/roll_up/variants/__init__.py b/pm4py/algo/transformation/ocel/olap/roll_up/variants/__init__.py
new file mode 100644
index 000000000..712852e4c
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/roll_up/variants/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.roll_up.variants import classic
diff --git a/pm4py/algo/transformation/ocel/olap/roll_up/variants/classic.py b/pm4py/algo/transformation/ocel/olap/roll_up/variants/classic.py
new file mode 100644
index 000000000..1a06bbd50
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/roll_up/variants/classic.py
@@ -0,0 +1,117 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+import copy as copy_mod
+from enum import Enum
+from typing import Any, Callable, Dict, Optional
+
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Parameters(Enum):
+ OBJECT_TYPE = "object_type"
+ OBJECT_ATTRIBUTE = "object_attribute"
+ TUPLE_MATCHER = "tuple_matcher"
+
+
+def _default_matcher(type_value: Any, parent: str) -> bool:
+ """Recognizes the default ``"(parent, value)"`` tuple encoding."""
+ if not isinstance(type_value, str):
+ return False
+ if type_value == parent:
+ return False
+ return type_value.startswith("(" + parent + ", ") and type_value.endswith(
+ ")"
+ )
+
+
+def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> OCEL:
+ """
+ Roll-up: the inverse of drill-down. Collapses tuple-style sub-types of
+ a given parent object type back to the parent type.
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ parameters
+ - Parameters.OBJECT_TYPE: name of the parent object type to merge
+ back to (required)
+ - Parameters.OBJECT_ATTRIBUTE: optional, accepted for API symmetry
+ with drill_down; if provided, must be a column of
+ ``ocel.objects``. Not semantically required because the tuple
+ type value already encodes the split.
+ - Parameters.TUPLE_MATCHER: optional callable
+ ``(type_value, parent) -> bool`` deciding whether ``type_value``
+ is a sub-type of ``parent``. Default matches the
+ ``"(parent, value)"`` encoding produced by drill_down.
+
+ Returns a new OCEL; the input log is left untouched.
+ """
+ if parameters is None:
+ parameters = {}
+
+ ot = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, None)
+ oa = exec_utils.get_param_value(
+ Parameters.OBJECT_ATTRIBUTE, parameters, None
+ )
+ matcher: Callable[[Any, str], bool] = exec_utils.get_param_value(
+ Parameters.TUPLE_MATCHER, parameters, _default_matcher
+ )
+
+ if ot is None:
+ raise ValueError("roll_up requires Parameters.OBJECT_TYPE")
+
+ result = copy_mod.deepcopy(ocel)
+ type_col = result.object_type_column
+ oid_col = result.object_id_column
+
+ if oa is not None and oa not in result.objects.columns:
+ raise ValueError(
+ "object attribute %r is not a column of ocel.objects" % (oa,)
+ )
+
+ mask = result.objects[type_col].map(lambda t: matcher(t, ot))
+
+ if mask.any():
+ result.objects.loc[mask, type_col] = ot
+
+ oid_to_type = dict(
+ zip(result.objects[oid_col], result.objects[type_col])
+ )
+ result.relations[type_col] = (
+ result.relations[oid_col]
+ .map(oid_to_type)
+ .fillna(result.relations[type_col])
+ )
+
+ if (
+ type_col in result.object_changes.columns
+ and len(result.object_changes) > 0
+ ):
+ result.object_changes[type_col] = (
+ result.object_changes[oid_col]
+ .map(oid_to_type)
+ .fillna(result.object_changes[type_col])
+ )
+
+ return result
diff --git a/pm4py/algo/transformation/ocel/olap/unfold/__init__.py b/pm4py/algo/transformation/ocel/olap/unfold/__init__.py
new file mode 100644
index 000000000..47f0b4b72
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/unfold/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.unfold import algorithm
diff --git a/pm4py/algo/transformation/ocel/olap/unfold/algorithm.py b/pm4py/algo/transformation/ocel/olap/unfold/algorithm.py
new file mode 100644
index 000000000..bef5a24f9
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/unfold/algorithm.py
@@ -0,0 +1,66 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from enum import Enum
+from typing import Any, Dict, Optional
+
+from pm4py.algo.transformation.ocel.olap.unfold.variants import classic
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Variants(Enum):
+ CLASSIC = classic
+
+
+def apply(
+ ocel: OCEL,
+ variant=Variants.CLASSIC,
+ parameters: Optional[Dict[Any, Any]] = None,
+) -> OCEL:
+ """
+ Applies the unfold OLAP operation on an object-centric event log,
+ splitting an event type into a tuple-style sub-type keyed by a related
+ object type (optionally filtered by a qualifier set).
+
+ Reference: Khayatbashi, Miri, Jalali. "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations." CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ variant
+ Variant of the algorithm to be used, possible values:
+ - Variants.CLASSIC
+ parameters
+ Variant-specific parameters
+
+ Returns
+ --------------
+ new_ocel
+ A new object-centric event log with the unfolded event type.
+ """
+ if parameters is None:
+ parameters = {}
+
+ return exec_utils.get_variant(variant).apply(ocel, parameters=parameters)
diff --git a/pm4py/algo/transformation/ocel/olap/unfold/variants/__init__.py b/pm4py/algo/transformation/ocel/olap/unfold/variants/__init__.py
new file mode 100644
index 000000000..93ac05107
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/unfold/variants/__init__.py
@@ -0,0 +1,22 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+from pm4py.algo.transformation.ocel.olap.unfold.variants import classic
diff --git a/pm4py/algo/transformation/ocel/olap/unfold/variants/classic.py b/pm4py/algo/transformation/ocel/olap/unfold/variants/classic.py
new file mode 100644
index 000000000..9eb1f6cbd
--- /dev/null
+++ b/pm4py/algo/transformation/ocel/olap/unfold/variants/classic.py
@@ -0,0 +1,107 @@
+'''
+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 .
+
+Website: https://processintelligence.solutions
+Contact: info@processintelligence.solutions
+'''
+import copy as copy_mod
+from enum import Enum
+from typing import Any, Callable, Dict, Iterable, Optional
+
+from pm4py.objects.ocel.obj import OCEL
+from pm4py.util import exec_utils
+
+
+class Parameters(Enum):
+ EVENT_TYPE = "event_type"
+ OBJECT_TYPE = "object_type"
+ QUALIFIERS = "qualifiers"
+ TUPLE_FORMAT = "tuple_format"
+
+
+def _default_format(parent: str, value: str) -> str:
+ return "(" + parent + ", " + value + ")"
+
+
+def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None) -> OCEL:
+ """
+ Unfold: splits an event type into a tuple-style sub-type keyed by a
+ related object type.
+
+ For every event of type ``event_type`` that has at least one E2O
+ relation to an object of type ``object_type`` (optionally restricted
+ to qualifiers in ``qualifiers``), the event's activity is rewritten
+ to ``tuple_format(event_type, object_type)`` in both ``ocel.events``
+ and ``ocel.relations``.
+
+ Parameters
+ --------------
+ ocel
+ Object-centric event log
+ parameters
+ - Parameters.EVENT_TYPE: the event type/activity to unfold (required)
+ - Parameters.OBJECT_TYPE: the related object type to unfold over
+ (required)
+ - Parameters.QUALIFIERS: optional iterable of qualifier values;
+ when provided, only E2O relations whose qualifier is in this
+ set contribute to the unfolding. Default: all qualifiers.
+ - Parameters.TUPLE_FORMAT: optional callable
+ ``(event_type, object_type) -> str`` producing the new activity
+ name. Must match fold's format to be reversible. Default:
+ ``"(event_type, object_type)"``.
+
+ Returns a new OCEL; the input log is left untouched.
+ """
+ if parameters is None:
+ parameters = {}
+
+ et = exec_utils.get_param_value(Parameters.EVENT_TYPE, parameters, None)
+ ot = exec_utils.get_param_value(Parameters.OBJECT_TYPE, parameters, None)
+ qualifiers: Optional[Iterable[Any]] = exec_utils.get_param_value(
+ Parameters.QUALIFIERS, parameters, None
+ )
+ fmt: Callable[[str, str], str] = exec_utils.get_param_value(
+ Parameters.TUPLE_FORMAT, parameters, _default_format
+ )
+
+ if et is None:
+ raise ValueError("unfold requires Parameters.EVENT_TYPE")
+ if ot is None:
+ raise ValueError("unfold requires Parameters.OBJECT_TYPE")
+
+ result = copy_mod.deepcopy(ocel)
+ eid_col = result.event_id_column
+ act_col = result.event_activity
+ type_col = result.object_type_column
+ qual_col = result.qualifier
+
+ rel = result.relations
+ mask = (rel[act_col] == et) & (rel[type_col] == ot)
+ if qualifiers is not None:
+ mask = mask & rel[qual_col].isin(list(qualifiers))
+
+ matched_eids = set(rel.loc[mask, eid_col].unique())
+
+ if matched_eids:
+ new_act = fmt(et, ot)
+ ev_mask = result.events[eid_col].isin(matched_eids)
+ result.events.loc[ev_mask, act_col] = new_act
+ rel_mask = result.relations[eid_col].isin(matched_eids)
+ result.relations.loc[rel_mask, act_col] = new_act
+
+ return result
diff --git a/pm4py/ocel.py b/pm4py/ocel.py
index 4304d8edd..dfb796e73 100644
--- a/pm4py/ocel.py
+++ b/pm4py/ocel.py
@@ -841,3 +841,187 @@ def cluster_equivalent_ocel(
ret[descr] = []
ret[descr].append(oc)
return ret
+
+
+def ocel_drill_down(
+ ocel: OCEL, object_type: str, object_attribute: str
+) -> OCEL:
+ """
+ Drill-down OLAP operation on an object-centric event log: splits a given
+ object type into finer-grained tuple-style sub-types based on the values
+ of a given object attribute.
+
+ For every object whose ``ocel:type`` equals ``object_type`` and whose
+ ``object_attribute`` value is defined, the type is rewritten to
+ ``"(object_type, value)"``. Objects with an undefined attribute value
+ keep their original type. The operation is information-preserving and
+ reversible via :func:`ocel_roll_up`.
+
+ Reference: Khayatbashi, Miri, Jalali, "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations", CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ :param ocel: Object-centric event log.
+ :type ocel: OCEL
+ :param object_type: Object type to split.
+ :type object_type: str
+ :param object_attribute: Object attribute column whose values drive
+ the split.
+ :type object_attribute: str
+ :return: A new OCEL with the drilled-down object type.
+ :rtype: OCEL
+
+ .. code-block:: python3
+
+ import pm4py
+
+ drilled = pm4py.ocel_drill_down(ocel, "Test", "test_type")
+ """
+ from pm4py.algo.transformation.ocel.olap.drill_down import (
+ algorithm as drill_down_algorithm,
+ )
+ from pm4py.algo.transformation.ocel.olap.drill_down.variants import (
+ classic as drill_down_classic,
+ )
+
+ parameters = {
+ drill_down_classic.Parameters.OBJECT_TYPE: object_type,
+ drill_down_classic.Parameters.OBJECT_ATTRIBUTE: object_attribute,
+ }
+ return drill_down_algorithm.apply(ocel, parameters=parameters)
+
+
+def ocel_roll_up(
+ ocel: OCEL, object_type: str, object_attribute: Optional[str] = None
+) -> OCEL:
+ """
+ Roll-up OLAP operation on an object-centric event log: collapses
+ tuple-style sub-types of the given parent ``object_type`` back to the
+ parent type. Inverse of :func:`ocel_drill_down`.
+
+ Reference: Khayatbashi, Miri, Jalali, "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations", CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ :param ocel: Object-centric event log.
+ :type ocel: OCEL
+ :param object_type: Parent object type to merge sub-types back to.
+ :type object_type: str
+ :param object_attribute: Optional, accepted for API symmetry with
+ :func:`ocel_drill_down`. When provided it must be a column of
+ ``ocel.objects``, but it is not semantically required because the
+ tuple type value already encodes the split.
+ :type object_attribute: Optional[str]
+ :return: A new OCEL with the rolled-up object type.
+ :rtype: OCEL
+
+ .. code-block:: python3
+
+ import pm4py
+
+ rolled = pm4py.ocel_roll_up(drilled, "Test")
+ """
+ from pm4py.algo.transformation.ocel.olap.roll_up import (
+ algorithm as roll_up_algorithm,
+ )
+ from pm4py.algo.transformation.ocel.olap.roll_up.variants import (
+ classic as roll_up_classic,
+ )
+
+ parameters = {
+ roll_up_classic.Parameters.OBJECT_TYPE: object_type,
+ roll_up_classic.Parameters.OBJECT_ATTRIBUTE: object_attribute,
+ }
+ return roll_up_algorithm.apply(ocel, parameters=parameters)
+
+
+def ocel_unfold(
+ ocel: OCEL,
+ event_type: str,
+ object_type: str,
+ qualifiers: Optional[Collection[Any]] = None,
+) -> OCEL:
+ """
+ Unfold OLAP operation on an object-centric event log: splits an event
+ type into a tuple-style sub-type keyed by a related object type,
+ optionally restricted to a set of E2O qualifiers.
+
+ Every event of type ``event_type`` that has at least one matching
+ E2O relation to an object of type ``object_type`` has its activity
+ rewritten to ``"(event_type, object_type)"`` in both ``ocel.events``
+ and ``ocel.relations``. Reversible via :func:`ocel_fold`.
+
+ Reference: Khayatbashi, Miri, Jalali, "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations", CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ :param ocel: Object-centric event log.
+ :type ocel: OCEL
+ :param event_type: Event type/activity to unfold.
+ :type event_type: str
+ :param object_type: Related object type to unfold over.
+ :type object_type: str
+ :param qualifiers: Optional collection of qualifier values to filter
+ the driving E2O relations. ``None`` means all qualifiers.
+ :type qualifiers: Optional[Collection[Any]]
+ :return: A new OCEL with the unfolded event type.
+ :rtype: OCEL
+
+ .. code-block:: python3
+
+ import pm4py
+
+ unfolded = pm4py.ocel_unfold(ocel, "order test", "(Test, ECG)")
+ """
+ from pm4py.algo.transformation.ocel.olap.unfold import (
+ algorithm as unfold_algorithm,
+ )
+ from pm4py.algo.transformation.ocel.olap.unfold.variants import (
+ classic as unfold_classic,
+ )
+
+ parameters = {
+ unfold_classic.Parameters.EVENT_TYPE: event_type,
+ unfold_classic.Parameters.OBJECT_TYPE: object_type,
+ unfold_classic.Parameters.QUALIFIERS: qualifiers,
+ }
+ return unfold_algorithm.apply(ocel, parameters=parameters)
+
+
+def ocel_fold(ocel: OCEL, event_type: str, object_type: str) -> OCEL:
+ """
+ Fold OLAP operation on an object-centric event log: collapses the
+ unfolded activity ``"(event_type, object_type)"`` back to
+ ``event_type``. Inverse of :func:`ocel_unfold`.
+
+ Reference: Khayatbashi, Miri, Jalali, "Advancing Object-Centric Process
+ Mining with Multi-Dimensional Data Operations", CAiSE Forum 2025
+ (arXiv:2412.00393).
+
+ :param ocel: Object-centric event log.
+ :type ocel: OCEL
+ :param event_type: Parent event type to merge back to.
+ :type event_type: str
+ :param object_type: Object type used during the matching unfold.
+ :type object_type: str
+ :return: A new OCEL with the folded event type.
+ :rtype: OCEL
+
+ .. code-block:: python3
+
+ import pm4py
+
+ folded = pm4py.ocel_fold(unfolded, "order test", "(Test, ECG)")
+ """
+ from pm4py.algo.transformation.ocel.olap.fold import (
+ algorithm as fold_algorithm,
+ )
+ from pm4py.algo.transformation.ocel.olap.fold.variants import (
+ classic as fold_classic,
+ )
+
+ parameters = {
+ fold_classic.Parameters.EVENT_TYPE: event_type,
+ fold_classic.Parameters.OBJECT_TYPE: object_type,
+ }
+ return fold_algorithm.apply(ocel, parameters=parameters)
diff --git a/tests/ocel_olap_test.py b/tests/ocel_olap_test.py
new file mode 100644
index 000000000..0baa9bd27
--- /dev/null
+++ b/tests/ocel_olap_test.py
@@ -0,0 +1,242 @@
+import os
+import unittest
+
+import pm4py
+from pm4py.objects.ocel.obj import OCEL
+
+
+LOG_PATH = os.path.join("input_data", "ocel", "example_log.jsonocel")
+
+
+class OcelOlapTest(unittest.TestCase):
+ def _load(self):
+ return pm4py.read_ocel(LOG_PATH)
+
+ def test_drill_down_splits_types(self):
+ ocel = self._load()
+ type_col = ocel.object_type_column
+ original_types = set(ocel.objects[type_col].unique())
+
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+
+ self.assertIsInstance(drilled, OCEL)
+ new_types = set(drilled.objects[type_col].unique())
+ # The drilled log must contain at least one tuple-style sub-type.
+ self.assertTrue(
+ any(isinstance(t, str) and t.startswith("(element, ") for t in new_types)
+ )
+ # More distinct types than before (element splits into several).
+ self.assertGreater(len(new_types), len(original_types))
+ # Relations table is synced with the new object types.
+ rel_types = set(drilled.relations[type_col].unique())
+ self.assertTrue(
+ any(isinstance(t, str) and t.startswith("(element, ") for t in rel_types)
+ )
+ # Input log was not mutated.
+ self.assertEqual(set(ocel.objects[type_col].unique()), original_types)
+
+ def test_drill_down_skips_undefined(self):
+ ocel = self._load()
+ type_col = ocel.object_type_column
+ oid_col = ocel.object_id_column
+
+ # "i4" has NaN oattr1 in the example log; it must retain the
+ # original "element" type.
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ i4_rows = drilled.objects[drilled.objects[oid_col] == "i4"]
+ self.assertEqual(len(i4_rows), 1)
+ self.assertEqual(i4_rows.iloc[0][type_col], "element")
+
+ def test_drill_down_roll_up_roundtrip(self):
+ ocel = self._load()
+ type_col = ocel.object_type_column
+
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ rolled = pm4py.ocel_roll_up(drilled, object_type="element")
+
+ self.assertTrue(
+ rolled.objects[type_col]
+ .reset_index(drop=True)
+ .equals(ocel.objects[type_col].reset_index(drop=True))
+ )
+ self.assertTrue(
+ rolled.relations[type_col]
+ .reset_index(drop=True)
+ .equals(ocel.relations[type_col].reset_index(drop=True))
+ )
+
+ def test_unfold_splits_event_type(self):
+ ocel = self._load()
+ act_col = ocel.event_activity
+
+ unfolded = pm4py.ocel_unfold(
+ ocel, event_type="Create Order", object_type="order"
+ )
+
+ self.assertIsInstance(unfolded, OCEL)
+ self.assertIn(
+ "(Create Order, order)",
+ set(unfolded.events[act_col].unique()),
+ )
+ self.assertIn(
+ "(Create Order, order)",
+ set(unfolded.relations[act_col].unique()),
+ )
+ # Input log was not mutated.
+ self.assertNotIn(
+ "(Create Order, order)", set(ocel.events[act_col].unique())
+ )
+
+ def test_unfold_fold_roundtrip(self):
+ ocel = self._load()
+ act_col = ocel.event_activity
+
+ unfolded = pm4py.ocel_unfold(
+ ocel, event_type="Create Order", object_type="order"
+ )
+ folded = pm4py.ocel_fold(
+ unfolded, event_type="Create Order", object_type="order"
+ )
+
+ self.assertTrue(
+ folded.events[act_col]
+ .reset_index(drop=True)
+ .equals(ocel.events[act_col].reset_index(drop=True))
+ )
+ self.assertTrue(
+ folded.relations[act_col]
+ .reset_index(drop=True)
+ .equals(ocel.relations[act_col].reset_index(drop=True))
+ )
+
+ def test_unfold_with_empty_qualifier_set_is_noop(self):
+ ocel = self._load()
+ act_col = ocel.event_activity
+
+ unfolded = pm4py.ocel_unfold(
+ ocel,
+ event_type="Create Order",
+ object_type="order",
+ qualifiers=[],
+ )
+ self.assertNotIn(
+ "(Create Order, order)",
+ set(unfolded.events[act_col].unique()),
+ )
+
+ def test_drill_down_invalid_object_type_raises(self):
+ ocel = self._load()
+ with self.assertRaises(ValueError):
+ pm4py.ocel_drill_down(
+ ocel,
+ object_type="__no_such_type__",
+ object_attribute="oattr1",
+ )
+
+ def test_drill_down_invalid_attribute_raises(self):
+ ocel = self._load()
+ with self.assertRaises(ValueError):
+ pm4py.ocel_drill_down(
+ ocel,
+ object_type="element",
+ object_attribute="__no_such_attr__",
+ )
+
+ def test_drilled_log_still_discovers_ocdfg(self):
+ ocel = self._load()
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ # Smoke test: downstream OC-DFG discovery must still work.
+ ocdfg = pm4py.discover_ocdfg(drilled)
+ self.assertIsNotNone(ocdfg)
+
+ def test_drill_down_roll_up_full_equality(self):
+ # Stronger than the column-level round-trip above: the rolled-up
+ # OCEL must be fully equal to the original via OCEL.__eq__.
+ ocel = self._load()
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ rolled = pm4py.ocel_roll_up(drilled, object_type="element")
+ self.assertEqual(rolled, ocel)
+
+ def test_unfold_fold_full_equality(self):
+ ocel = self._load()
+ unfolded = pm4py.ocel_unfold(
+ ocel, event_type="Create Order", object_type="order"
+ )
+ folded = pm4py.ocel_fold(
+ unfolded, event_type="Create Order", object_type="order"
+ )
+ self.assertEqual(folded, ocel)
+
+ def test_drill_down_is_idempotent(self):
+ ocel = self._load()
+ once = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ twice = pm4py.ocel_drill_down(
+ once, object_type="element", object_attribute="oattr1"
+ )
+ # Applying drill-down a second time must be a no-op: the "element"
+ # rows left after the first pass have undefined oattr1 and are
+ # skipped, and the already-drilled tuple-typed rows no longer match
+ # the target type.
+ self.assertEqual(twice, once)
+
+ def test_drill_down_preserves_auxiliary_tables(self):
+ ocel = self._load()
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ self.assertTrue(drilled.o2o.equals(ocel.o2o))
+ self.assertTrue(drilled.e2e.equals(ocel.e2e))
+ self.assertTrue(
+ drilled.object_changes.equals(ocel.object_changes)
+ )
+
+ def test_drill_down_assigns_expected_tuple_values(self):
+ # Ground-truth check: verify specific objects receive the exact
+ # tuple-typed name derived from their oattr1 value. Without this
+ # it would be possible for drill-down to mis-align values with
+ # objects (e.g. due to a faulty mask/list assignment).
+ ocel = self._load()
+ drilled = pm4py.ocel_drill_down(
+ ocel, object_type="element", object_attribute="oattr1"
+ )
+ type_col = drilled.object_type_column
+ oid_col = drilled.object_id_column
+ indexed = drilled.objects.set_index(oid_col)[type_col].to_dict()
+ self.assertEqual(indexed["i1"], "(element, due)")
+ self.assertEqual(indexed["i2"], "(element, tre)")
+ self.assertEqual(indexed["i3"], "(element, quattro)")
+ # i4 has NaN oattr1 and must keep the original type.
+ self.assertEqual(indexed["i4"], "element")
+
+ def test_unfold_with_matching_qualifier(self):
+ # Positive case for the qualifier filter: the example log uses the
+ # empty-string qualifier for every E2O relation, so passing it
+ # explicitly must match and rename just like the default None.
+ ocel = self._load()
+ act_col = ocel.event_activity
+ unfolded = pm4py.ocel_unfold(
+ ocel,
+ event_type="Create Order",
+ object_type="order",
+ qualifiers=[""],
+ )
+ self.assertIn(
+ "(Create Order, order)",
+ set(unfolded.events[act_col].unique()),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()