Skip to content

Commit 365cf50

Browse files
committed
Merge remote-tracking branch 'upstream/hotfixes' into release
2 parents 605ff5c + 3524422 commit 365cf50

5 files changed

Lines changed: 92 additions & 4 deletions

File tree

pm4py/discovery.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ def discover_performance_dfg(
240240
:param timestamp_key: Attribute to be used for the timestamp (default: "time:timestamp").
241241
:param case_id_key: Attribute to be used as case identifier (default: "case:concept:name").
242242
:param perf_aggregation_key: Selector for the type of aggregation (all, mean, median, max, min, sum, stdev)
243+
When business hours are enabled, the returned performance DFG remains
244+
dictionary-compatible and also retains the configured schedule so that
245+
visualizations can express its durations in working days.
246+
243247
:return: A tuple of three dictionaries: (performance_dfg, start_activities, end_activities).
244248
:rtype: ``Tuple[dict, dict, dict]``
245249
@@ -332,6 +336,13 @@ def discover_performance_dfg(
332336
end_activities = end_activities_module.get_end_activities(
333337
log, parameters=properties
334338
)
339+
if business_hours:
340+
from pm4py.objects.dfg.obj import PerformanceDFG
341+
342+
dfg = PerformanceDFG(
343+
dfg, business_hour_slots=business_hour_slots
344+
)
345+
335346
return dfg, start_activities, end_activities
336347

337348

pm4py/objects/dfg/obj.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@
2323
from typing import Tuple, Any, Counter as TCounter
2424

2525

26+
class PerformanceDFG(dict):
27+
"""Dictionary-compatible performance DFG with calculation metadata.
28+
29+
Performance values are intentionally kept as a regular mapping for
30+
backwards compatibility. The optional business-hour schedule lets
31+
downstream consumers render those values using the same definition of a
32+
working day that was used during discovery.
33+
"""
34+
35+
def __init__(self, *args, business_hour_slots=None, **kwargs):
36+
super().__init__(*args, **kwargs)
37+
self.business_hour_slots = (
38+
tuple(tuple(slot) for slot in business_hour_slots)
39+
if business_hour_slots is not None
40+
else None
41+
)
42+
43+
def copy(self):
44+
return type(self)(
45+
self, business_hour_slots=self.business_hour_slots
46+
)
47+
48+
2649
class DirectlyFollowsGraph:
2750

2851
def __init__(self, graph=None, start_activities=None, end_activities=None):

pm4py/util/vis_utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
MIN_EDGE_PENWIDTH_GRAPHVIZ = 1.0
3131

3232

33-
def get_business_hour_slots(parameters):
34-
"""Return the schedule to use for duration labels, if configured."""
33+
def get_business_hour_slots(parameters, performance_dfg=None):
34+
"""Return the schedule to use for duration labels, if configured.
35+
36+
Explicit visualization parameters take precedence over metadata retained
37+
by a discovered performance DFG.
38+
"""
3539
from pm4py.util import constants, exec_utils
3640

3741
missing = object()
@@ -43,8 +47,14 @@ def get_business_hour_slots(parameters):
4347
)
4448
if business_hours is not missing and not business_hours:
4549
return None
46-
if business_hour_slots is not missing:
50+
if business_hour_slots is not missing and business_hour_slots is not None:
4751
return business_hour_slots
52+
if performance_dfg is not None:
53+
discovered_slots = getattr(
54+
performance_dfg, "business_hour_slots", None
55+
)
56+
if discovered_slots is not None:
57+
return discovered_slots
4858
if business_hours is not missing and business_hours:
4959
return constants.DEFAULT_BUSINESS_HOUR_SLOTS
5060
return None

pm4py/visualization/dfg/variants/performance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def apply(
125125
parameters,
126126
"Performance Directly-Follows Graph",
127127
)
128-
business_hour_slots = vis_utils.get_business_hour_slots(parameters)
128+
business_hour_slots = vis_utils.get_business_hour_slots(parameters, dfg)
129129

130130
# if all the aggregation measures are provided for a given key,
131131
# then pick one of the values for the representation

tests/business_hours_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import random
22
import unittest
33
from datetime import date, datetime, timedelta, timezone
4+
from unittest.mock import patch
45

56
from pm4py.util.business_hours import (
67
BusinessHours,
@@ -58,6 +59,49 @@ def test_performance_dfg_uses_business_days_in_labels(self):
5859

5960
self.assertIn('label="4D"', graph.source)
6061

62+
def test_discovered_performance_dfg_retains_business_hour_slots(self):
63+
import pm4py
64+
from pm4py.discovery import discover_performance_dfg
65+
from pm4py.objects.log.obj import Event, EventLog, Trace
66+
67+
slots = weekday_slots(8, 16)
68+
trace = Trace(
69+
[
70+
Event(
71+
{
72+
"concept:name": "start",
73+
"time:timestamp": datetime(2025, 1, 7, 8),
74+
}
75+
),
76+
Event(
77+
{
78+
"concept:name": "finish",
79+
"time:timestamp": datetime(2025, 1, 10, 16),
80+
}
81+
),
82+
]
83+
)
84+
85+
dfg, start_activities, end_activities = discover_performance_dfg(
86+
EventLog([trace]),
87+
business_hours=True,
88+
business_hour_slots=slots,
89+
perf_aggregation_key="median",
90+
)
91+
with patch("pm4py.visualization.dfg.visualizer.view") as view:
92+
pm4py.view_performance_dfg(
93+
dfg,
94+
start_activities,
95+
end_activities,
96+
aggregation_measure="median",
97+
)
98+
graph = view.call_args.args[0]
99+
100+
self.assertIsInstance(dfg, dict)
101+
self.assertEqual(4 * 8 * 60 * 60, dfg[("start", "finish")])
102+
self.assertEqual(tuple(slots), dfg.business_hour_slots)
103+
self.assertIn('label="4D"', graph.source)
104+
61105
def test_variant_duration_uses_configured_working_day(self):
62106
from pm4py.visualization.variants_duration.variants.classic import (
63107
_format_duration,

0 commit comments

Comments
 (0)