ocelescope-module-alignments 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ocelescope_module_alignments-0.1.0/PKG-INFO +27 -0
- ocelescope_module_alignments-0.1.0/README.md +16 -0
- ocelescope_module_alignments-0.1.0/pyproject.toml +24 -0
- ocelescope_module_alignments-0.1.0/pyproject.toml.orig +22 -0
- ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/__init__.py +0 -0
- ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/models/alignment.py +69 -0
- ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/module.py +14 -0
- ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/routes.py +45 -0
- ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/utils/compute_alignments.py +245 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ocelescope-module-alignments
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: case-centric Alignments
|
|
5
|
+
Author: Uy Sa Huynh
|
|
6
|
+
Requires-Dist: ocelescope-backend>=0.10.1
|
|
7
|
+
Requires-Dist: ocelescope-module-filter>=0.10.1,<0.11.0
|
|
8
|
+
Requires-Dist: ocelescope-module-ocel>=0.10.1,<0.11.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# ocelescope-module-case-centric-alignments
|
|
13
|
+
|
|
14
|
+
An [Ocelescope](https://www.ocelescope.org) backend module that computes
|
|
15
|
+
case-centric alignments for object-centric event logs.
|
|
16
|
+
|
|
17
|
+
For a selected object type, the log is aligned against a Petri net resource,
|
|
18
|
+
or against a model discovered with Alpha+++ if none is given.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install ocelescope-module-alignments
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install it next to `ocelescope-backend`. The module is picked up automatically
|
|
27
|
+
and served under `/modules/alignments/v1`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ocelescope-module-case-centric-alignments
|
|
2
|
+
|
|
3
|
+
An [Ocelescope](https://www.ocelescope.org) backend module that computes
|
|
4
|
+
case-centric alignments for object-centric event logs.
|
|
5
|
+
|
|
6
|
+
For a selected object type, the log is aligned against a Petri net resource,
|
|
7
|
+
or against a model discovered with Alpha+++ if none is given.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install ocelescope-module-alignments
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install it next to `ocelescope-backend`. The module is picked up automatically
|
|
16
|
+
and served under `/modules/alignments/v1`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ocelescope-module-alignments"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
description = "case-centric Alignments"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"ocelescope-backend>=0.10.1",
|
|
9
|
+
"ocelescope-module-filter>=0.10.1,<0.11.0",
|
|
10
|
+
"ocelescope-module-ocel>=0.10.1,<0.11.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "Uy Sa Huynh"
|
|
15
|
+
|
|
16
|
+
[project.entry-points."ocelescope_backend.modules"]
|
|
17
|
+
caseCentricAlignments = "ocelescope_module_alignments.module:Alignments"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.9.26,<0.12.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
[tool.uv]
|
|
24
|
+
package = true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ocelescope-module-alignments"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
description = "case-centric Alignments"
|
|
6
|
+
authors = [{ name = "Uy Sa Huynh" }]
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"ocelescope-backend>=0.10.1",
|
|
10
|
+
"ocelescope-module-filter>=0.10.1,<0.11.0",
|
|
11
|
+
"ocelescope-module-ocel>=0.10.1,<0.11.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.entry-points."ocelescope_backend.modules"]
|
|
15
|
+
caseCentricAlignments = "ocelescope_module_alignments.module:Alignments"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.9.26,<0.12.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[tool.uv]
|
|
22
|
+
package = true
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
2
|
+
|
|
3
|
+
from ocelescope import PetriNet
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VariantAlignmentResult(BaseModel):
|
|
8
|
+
activities: List[str] # The variant's activity sequence
|
|
9
|
+
frequency: int # How many traces follow this variant
|
|
10
|
+
result: Union[Dict[str, Any]] # The alignment result or error for this variant
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FitnessResult(BaseModel):
|
|
14
|
+
average_fitness: float # Average trace fitness (across all traces)
|
|
15
|
+
log_fitness: float # Log fitness, as the total computed fitness (summing up the costs for all traces)
|
|
16
|
+
perfectly_fitting_frac: float # Fraction of traces that perfectly fit (i.e., have an alignment cost of `0`)
|
|
17
|
+
total_costs: int # The total cost, summed up from all traces
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CostFunction(BaseModel):
|
|
21
|
+
log_move_cost: int # Default cost for a log move (log event not matched by model)
|
|
22
|
+
model_move_cost: int # Default cost for a model move (visible transition fires without matching log event)
|
|
23
|
+
silent_move_cost: int # Default cost for a silent/tau move
|
|
24
|
+
sync_move_cost: int # Default cost for a synchronous move
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AlignmentOptions(BaseModel):
|
|
28
|
+
cost_fn: CostFunction # Cost function for alignment moves
|
|
29
|
+
max_states: Optional[Optional[int]] # Maximum number of states to visit before aborting (per trace).
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TransitionStat(BaseModel):
|
|
33
|
+
sync_fires: int
|
|
34
|
+
model_fires: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Aggregated(BaseModel):
|
|
38
|
+
total_traces: int
|
|
39
|
+
transition_stats: Dict[str, TransitionStat]
|
|
40
|
+
log_move_counts: Dict[str, int]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NetPlace(BaseModel):
|
|
44
|
+
id: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NetTransition(BaseModel):
|
|
48
|
+
id: str
|
|
49
|
+
label: str | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NetArc(BaseModel):
|
|
53
|
+
nodes: Tuple[str, str]
|
|
54
|
+
weight: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResponseNet(BaseModel):
|
|
58
|
+
places: List[NetPlace]
|
|
59
|
+
transitions: List[NetTransition]
|
|
60
|
+
arcs: List[NetArc]
|
|
61
|
+
initial_marking: Dict[str, int]
|
|
62
|
+
final_marking: Dict[str, int]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AlignmentsResponse(BaseModel):
|
|
66
|
+
net: ResponseNet
|
|
67
|
+
variant_alignments: List[VariantAlignmentResult]
|
|
68
|
+
fitness: FitnessResult
|
|
69
|
+
aggregated: Aggregated
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from ocelescope_backend.app.modules import Module, ModuleMeta
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
|
|
5
|
+
from ocelescope_module_alignments.routes import router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Alignments(Module):
|
|
9
|
+
meta = ModuleMeta(key="alignments", version=Version("1.0"))
|
|
10
|
+
|
|
11
|
+
def create_app(self) -> FastAPI:
|
|
12
|
+
app = FastAPI(title="Case-Centric Alignments", version=str(self.meta.version))
|
|
13
|
+
app.include_router(router)
|
|
14
|
+
return app
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from ocelescope import PetriNet
|
|
5
|
+
from ocelescope_backend.app.dependencies import ApiOcel, ApiSession
|
|
6
|
+
from ocelescope_backend.app.sse_manager import ErrorNotification, sse_manager
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ocelescope_module_alignments.models.alignment import AlignmentsResponse
|
|
10
|
+
from ocelescope_module_alignments.utils.compute_alignments import (
|
|
11
|
+
AlignmentComputationError,
|
|
12
|
+
compute_alignments,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/{ocel_id}/alignments", operation_id="get_alignments")
|
|
19
|
+
def get_alignments(
|
|
20
|
+
ocel: ApiOcel, session: ApiSession, object_type: str, resource_id: str | None = None
|
|
21
|
+
) -> AlignmentsResponse:
|
|
22
|
+
try:
|
|
23
|
+
if resource_id:
|
|
24
|
+
resource_store = cast(PetriNet, session.get_resource(resource_id))
|
|
25
|
+
petri_net = PetriNet(**resource_store.data)
|
|
26
|
+
else:
|
|
27
|
+
petri_net = None
|
|
28
|
+
|
|
29
|
+
return compute_alignments(ocel, object_type, petri_net)
|
|
30
|
+
except AlignmentComputationError as e:
|
|
31
|
+
sse_manager.send_safe(
|
|
32
|
+
session_id=session.id,
|
|
33
|
+
message=ErrorNotification(
|
|
34
|
+
type="error",
|
|
35
|
+
title="Alignment computation failed",
|
|
36
|
+
message=str(e),
|
|
37
|
+
trace=e.trace,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/{ocel_id}/objects/types", operation_id="objectTypes")
|
|
44
|
+
def get_object_types(ocel: ApiOcel) -> list[str]:
|
|
45
|
+
return ocel.objects.types
|
ocelescope_module_alignments-0.1.0/src/ocelescope_module_alignments/utils/compute_alignments.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import polars
|
|
6
|
+
import r4pm
|
|
7
|
+
from ocelescope import OCEL, PetriNet
|
|
8
|
+
from ocelescope_module_alignments.models.alignment import (
|
|
9
|
+
Aggregated,
|
|
10
|
+
AlignmentOptions,
|
|
11
|
+
AlignmentsResponse,
|
|
12
|
+
FitnessResult,
|
|
13
|
+
NetArc,
|
|
14
|
+
NetPlace,
|
|
15
|
+
NetTransition,
|
|
16
|
+
ResponseNet,
|
|
17
|
+
TransitionStat,
|
|
18
|
+
VariantAlignmentResult,
|
|
19
|
+
)
|
|
20
|
+
from r4pm.bindings.core.event_data.object_centric.utils.flatten import flatten_ocel_on
|
|
21
|
+
from r4pm.bindings.discovery.case_centric.alphappp.full import PetriNet as R4PMPetriNet
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AlignmentComputationError(Exception):
|
|
25
|
+
def __init__(self, message: str, trace: str | None = None):
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.trace = trace or message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_r4pm_format(petri_net: PetriNet) -> R4PMPetriNet:
|
|
31
|
+
place_ids = {p.name: str(uuid.uuid4()) for p in petri_net.places}
|
|
32
|
+
transition_ids = {t.name: str(uuid.uuid4()) for t in petri_net.transitions}
|
|
33
|
+
|
|
34
|
+
def node_id(name: str) -> str:
|
|
35
|
+
return place_ids.get(name) or transition_ids[name]
|
|
36
|
+
|
|
37
|
+
places = {pid: {"id": pid} for pid in place_ids.values()}
|
|
38
|
+
|
|
39
|
+
transitions = {
|
|
40
|
+
tid: {"id": tid, "label": t.label} for t, tid in ((t, transition_ids[t.name]) for t in petri_net.transitions)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
arcs = [
|
|
44
|
+
{
|
|
45
|
+
"from_to": {
|
|
46
|
+
"type": "PlaceTransition" if arc.source in place_ids else "TransitionPlace",
|
|
47
|
+
"nodes": [node_id(arc.source), node_id(arc.target)],
|
|
48
|
+
},
|
|
49
|
+
"weight": arc.weight,
|
|
50
|
+
}
|
|
51
|
+
for arc in petri_net.arcs
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
initial_marking = {place_ids[name]: count for name, count in petri_net.initial_marking.items()}
|
|
55
|
+
final_marking = {place_ids[name]: count for name, count in petri_net.final_marking.items()}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"places": places,
|
|
59
|
+
"transitions": transitions,
|
|
60
|
+
"arcs": arcs,
|
|
61
|
+
"initial_marking": initial_marking,
|
|
62
|
+
"final_markings": [final_marking] if final_marking else [],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def flatten(petri_net: PetriNet, object_type: str) -> PetriNet:
|
|
67
|
+
place_names = {p.name for p in petri_net.places if p.object_type == object_type}
|
|
68
|
+
places_flatten = [p for p in petri_net.places if p.name in place_names]
|
|
69
|
+
|
|
70
|
+
arcs_flatten = []
|
|
71
|
+
connected_transition_names = set()
|
|
72
|
+
for arc in petri_net.arcs:
|
|
73
|
+
source_is_place = arc.source in place_names
|
|
74
|
+
target_is_place = arc.target in place_names
|
|
75
|
+
if not (source_is_place or target_is_place):
|
|
76
|
+
continue
|
|
77
|
+
arcs_flatten.append(arc)
|
|
78
|
+
if not source_is_place:
|
|
79
|
+
connected_transition_names.add(arc.source)
|
|
80
|
+
if not target_is_place:
|
|
81
|
+
connected_transition_names.add(arc.target)
|
|
82
|
+
|
|
83
|
+
transitions_flatten = [t for t in petri_net.transitions if t.name in connected_transition_names]
|
|
84
|
+
|
|
85
|
+
initial_marking_flatten = {name: count for name, count in petri_net.initial_marking.items() if name in place_names}
|
|
86
|
+
final_marking_flatten = {name: count for name, count in petri_net.final_marking.items() if name in place_names}
|
|
87
|
+
|
|
88
|
+
return PetriNet(
|
|
89
|
+
places=places_flatten,
|
|
90
|
+
transitions=transitions_flatten,
|
|
91
|
+
arcs=arcs_flatten,
|
|
92
|
+
initial_marking=initial_marking_flatten,
|
|
93
|
+
final_marking=final_marking_flatten,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def convert_net(petri_net: R4PMPetriNet) -> ResponseNet:
|
|
98
|
+
places = [NetPlace(id=p_id) for p_id in petri_net["places"].keys()]
|
|
99
|
+
transitions = [NetTransition(id=t_id, label=t.get("label")) for t_id, t in petri_net["transitions"].items()]
|
|
100
|
+
arcs = [
|
|
101
|
+
NetArc(
|
|
102
|
+
nodes=(arc["from_to"]["nodes"][0], arc["from_to"]["nodes"][1]),
|
|
103
|
+
weight=arc["weight"],
|
|
104
|
+
)
|
|
105
|
+
for arc in petri_net["arcs"]
|
|
106
|
+
]
|
|
107
|
+
initial_marking = petri_net.get("initial_marking") or {}
|
|
108
|
+
final_markings = petri_net.get("final_markings") or []
|
|
109
|
+
final_marking = final_markings[0] if final_markings else {}
|
|
110
|
+
|
|
111
|
+
return ResponseNet(
|
|
112
|
+
places=places,
|
|
113
|
+
transitions=transitions,
|
|
114
|
+
arcs=arcs,
|
|
115
|
+
initial_marking=initial_marking,
|
|
116
|
+
final_marking=final_marking,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_flattened(petri_net: PetriNet) -> bool:
|
|
121
|
+
object_types = set()
|
|
122
|
+
for place in petri_net.places:
|
|
123
|
+
object_types.add(place.object_type)
|
|
124
|
+
if len(object_types) > 1:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
return len(object_types) == 1
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def preprocessing(ocel: OCEL, object_type: str, petri_net: PetriNet | None) -> Tuple[str, R4PMPetriNet]:
|
|
131
|
+
config = {
|
|
132
|
+
"absolute_df_clean_thresh": 1,
|
|
133
|
+
"balance_thresh": 0.2,
|
|
134
|
+
"fitness_thresh": 0.2,
|
|
135
|
+
"log_repair_loop_df_thresh_rel": 0.2,
|
|
136
|
+
"log_repair_skip_df_thresh_rel": 0.2,
|
|
137
|
+
"relative_df_clean_thresh": 0.2,
|
|
138
|
+
"replay_thresh": 0.2,
|
|
139
|
+
}
|
|
140
|
+
event_log_df = ocel.sql(f"""
|
|
141
|
+
SELECT
|
|
142
|
+
rel."ocel:oid" as "case:concept:name",
|
|
143
|
+
ev."ocel:timestamp" as "time:timestamp",
|
|
144
|
+
ev."ocel:activity" as "concept:name"
|
|
145
|
+
FROM
|
|
146
|
+
e2o rel
|
|
147
|
+
RIGHT JOIN (
|
|
148
|
+
SELECT
|
|
149
|
+
*
|
|
150
|
+
FROM
|
|
151
|
+
objects
|
|
152
|
+
WHERE
|
|
153
|
+
"ocel:type" = '{object_type}'
|
|
154
|
+
) ob USING ("ocel:oid")
|
|
155
|
+
LEFT JOIN (
|
|
156
|
+
SELECT
|
|
157
|
+
*
|
|
158
|
+
FROM
|
|
159
|
+
events
|
|
160
|
+
) ev USING ("ocel:eid")
|
|
161
|
+
ORDER BY
|
|
162
|
+
("time:timestamp", "case:concept:name", "concept:name")
|
|
163
|
+
""").pl()
|
|
164
|
+
|
|
165
|
+
log_id = r4pm.import_item_from_df("EventLog", event_log_df)
|
|
166
|
+
proj_id = r4pm.bindings.log_to_activity_projection(log_id)
|
|
167
|
+
if petri_net:
|
|
168
|
+
if is_flattened(petri_net):
|
|
169
|
+
process_model = to_r4pm_format(petri_net)
|
|
170
|
+
else:
|
|
171
|
+
process_model = to_r4pm_format(flatten(petri_net, object_type))
|
|
172
|
+
else:
|
|
173
|
+
process_model = r4pm.bindings.discover_alphaplusplusplus(proj_id, config=config)
|
|
174
|
+
return proj_id, process_model
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def variant_alignment(
|
|
178
|
+
proj_id: str,
|
|
179
|
+
process_model: R4PMPetriNet,
|
|
180
|
+
options: Optional["AlignmentOptions"] = None,
|
|
181
|
+
) -> List[VariantAlignmentResult]:
|
|
182
|
+
raw_result = r4pm.bindings.align_variants(process_model, proj_id, options)
|
|
183
|
+
return [VariantAlignmentResult(**r) for r in raw_result]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def compute_fitness(alignments: List[VariantAlignmentResult], process_model: R4PMPetriNet) -> FitnessResult:
|
|
187
|
+
fitness = r4pm.bindings.compute_fitness([a.model_dump() for a in alignments], process_model)
|
|
188
|
+
return FitnessResult(**fitness)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compute_aggregated(
|
|
192
|
+
variant_alignments: List[VariantAlignmentResult],
|
|
193
|
+
) -> Aggregated:
|
|
194
|
+
total_traces = sum(v.frequency for v in variant_alignments)
|
|
195
|
+
transition_stats: Dict[str, Dict[str, int]] = {}
|
|
196
|
+
log_move_counts: Dict[str, int] = {}
|
|
197
|
+
|
|
198
|
+
for v in variant_alignments:
|
|
199
|
+
result = v.result
|
|
200
|
+
if "Ok" not in result:
|
|
201
|
+
continue
|
|
202
|
+
freq = v.frequency
|
|
203
|
+
for move in result["Ok"]["moves"]:
|
|
204
|
+
if "SyncMove" in move:
|
|
205
|
+
t = move["SyncMove"]["transition"]
|
|
206
|
+
stats = transition_stats.setdefault(t, {"sync_fires": 0, "model_fires": 0})
|
|
207
|
+
stats["sync_fires"] += freq
|
|
208
|
+
elif "ModelMove" in move:
|
|
209
|
+
t = move["ModelMove"]["transition"]
|
|
210
|
+
stats = transition_stats.setdefault(t, {"sync_fires": 0, "model_fires": 0})
|
|
211
|
+
stats["model_fires"] += freq
|
|
212
|
+
elif "LogMove" in move:
|
|
213
|
+
idx = move["LogMove"]["trace_event_index"]
|
|
214
|
+
activity = v.activities[idx]
|
|
215
|
+
log_move_counts[activity] = log_move_counts.get(activity, 0) + freq
|
|
216
|
+
|
|
217
|
+
return Aggregated(
|
|
218
|
+
total_traces=total_traces,
|
|
219
|
+
transition_stats={k: TransitionStat(**v) for k, v in transition_stats.items()},
|
|
220
|
+
log_move_counts=log_move_counts,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def compute_alignments(ocel: OCEL, object_type: str, petri_net: PetriNet | None):
|
|
225
|
+
(proj_id, process_model) = preprocessing(ocel, object_type, petri_net)
|
|
226
|
+
options = {
|
|
227
|
+
"cost_fn": {
|
|
228
|
+
"log_move_cost": 1,
|
|
229
|
+
"model_move_cost": 1,
|
|
230
|
+
"silent_move_cost": 0,
|
|
231
|
+
"sync_move_cost": 0,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try:
|
|
235
|
+
alignments = variant_alignment(proj_id, process_model, options)
|
|
236
|
+
fitness = compute_fitness(alignments, process_model)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
raise AlignmentComputationError(e, traceback.format_exc()) from e
|
|
239
|
+
aggregated = compute_aggregated(alignments)
|
|
240
|
+
return AlignmentsResponse(
|
|
241
|
+
net=convert_net(process_model),
|
|
242
|
+
variant_alignments=alignments,
|
|
243
|
+
fitness=fitness,
|
|
244
|
+
aggregated=aggregated,
|
|
245
|
+
)
|