Fix self-loop detection to use the Definition 6 self-relation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,11 +73,6 @@ class RefinedDirectlyFollowsGraph:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Build the refined DFG from lifecycle traces.
|
"""Build the refined DFG from lifecycle traces.
|
||||||
|
|
||||||
For each trace, scan for end events. After each
|
|
||||||
end event, collect all start events that occur
|
|
||||||
before the next end event. These form the
|
|
||||||
directly-follows pairs per Definition 6.
|
|
||||||
|
|
||||||
:param traces: The event log.
|
:param traces: The event log.
|
||||||
"""
|
"""
|
||||||
for trace, count in traces.items():
|
for trace, count in traces.items():
|
||||||
@@ -98,17 +93,6 @@ class RefinedDirectlyFollowsGraph:
|
|||||||
if lifecycle == "end":
|
if lifecycle == "end":
|
||||||
self.__sinks.add(node)
|
self.__sinks.add(node)
|
||||||
break
|
break
|
||||||
# Detect self-loops: activity with multiple
|
|
||||||
# complete lifecycles in a trace.
|
|
||||||
end_counts: dict[Node, int] = {}
|
|
||||||
for node, lifecycle in trace:
|
|
||||||
if lifecycle == "end":
|
|
||||||
end_counts[node] = (
|
|
||||||
end_counts.get(node, 0) + 1
|
|
||||||
)
|
|
||||||
for node, cnt in end_counts.items():
|
|
||||||
if cnt > 1:
|
|
||||||
self.__self_loops.add(node)
|
|
||||||
# Definition 6: ax ->r ay iff ay starts
|
# Definition 6: ax ->r ay iff ay starts
|
||||||
# after ax ends with no other end event
|
# after ax ends with no other end event
|
||||||
# between.
|
# between.
|
||||||
@@ -121,12 +105,8 @@ class RefinedDirectlyFollowsGraph:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Scan a single trace for refined DF relations.
|
"""Scan a single trace for refined DF relations.
|
||||||
|
|
||||||
Walk through events. When we see an end event
|
A self-relation ax ->r ax is recorded as a
|
||||||
for activity ax, record ax as a "pending source".
|
self-loop instead of an edge.
|
||||||
When we see a start event for ay, create edges
|
|
||||||
from all pending sources to ay. When we see
|
|
||||||
another end event, clear all pending sources
|
|
||||||
(since the new end event is "between").
|
|
||||||
|
|
||||||
:param trace: The lifecycle trace.
|
:param trace: The lifecycle trace.
|
||||||
:param count: The trace frequency.
|
:param count: The trace frequency.
|
||||||
@@ -144,14 +124,14 @@ class RefinedDirectlyFollowsGraph:
|
|||||||
# All pending sources directly-follow
|
# All pending sources directly-follow
|
||||||
# to this activity.
|
# to this activity.
|
||||||
for src in pending:
|
for src in pending:
|
||||||
if src != node:
|
if src == node:
|
||||||
|
self.__self_loops.add(node)
|
||||||
|
continue
|
||||||
pair: tuple[Node, Node] = (
|
pair: tuple[Node, Node] = (
|
||||||
src, node
|
src, node
|
||||||
)
|
)
|
||||||
self.__df_freq[pair] = (
|
self.__df_freq[pair] = (
|
||||||
self.__df_freq.get(
|
self.__df_freq.get(pair, 0)
|
||||||
pair, 0
|
|
||||||
)
|
|
||||||
+ count
|
+ count
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -190,8 +170,11 @@ class RefinedDirectlyFollowsGraph:
|
|||||||
def self_loops(self) -> set[Node]:
|
def self_loops(self) -> set[Node]:
|
||||||
"""The set of self-loop nodes.
|
"""The set of self-loop nodes.
|
||||||
|
|
||||||
An activity is a self-loop if it completes
|
An activity a is a self-loop if a ->r a holds per
|
||||||
(has an end event) more than once in any trace.
|
Definition 6, that is, in some trace an end event
|
||||||
|
of a is followed by a start event of a with no
|
||||||
|
other end event in between. Such self-relations
|
||||||
|
are excluded from :attr:`edges`.
|
||||||
|
|
||||||
:return: The self-loop nodes.
|
:return: The self-loop nodes.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+123
-6
@@ -455,15 +455,16 @@ class TestForkAfterEnd(unittest.TestCase):
|
|||||||
class TestSelfLoop(unittest.TestCase):
|
class TestSelfLoop(unittest.TestCase):
|
||||||
"""Tests self-loop detection from lifecycle traces.
|
"""Tests self-loop detection from lifecycle traces.
|
||||||
|
|
||||||
An activity that completes more than once in a trace
|
An activity a is a self-loop iff a ->r a holds per
|
||||||
is a self-loop.
|
Definition 6, that is, an end event of a is followed by
|
||||||
|
a start event of a with no other end event in between.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
"""Set up trace with self-loop: A repeats.
|
"""Set up trace with self-loop: A repeats at once.
|
||||||
|
|
||||||
Trace: As Ae Bs Be As Ae Cs Ce
|
Trace: As Ae As Ae Bs Be Cs Ce
|
||||||
Activity A completes twice.
|
A ends and immediately starts again, so A ->r A.
|
||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
@@ -477,8 +478,8 @@ class TestSelfLoop(unittest.TestCase):
|
|||||||
tuple[tuple[Task, str], ...], int
|
tuple[tuple[Task, str], ...], int
|
||||||
] = {
|
] = {
|
||||||
((a, S), (a, E),
|
((a, S), (a, E),
|
||||||
(b, S), (b, E),
|
|
||||||
(a, S), (a, E),
|
(a, S), (a, E),
|
||||||
|
(b, S), (b, E),
|
||||||
(c, S), (c, E)): 1,
|
(c, S), (c, E)): 1,
|
||||||
}
|
}
|
||||||
self.__dfg: RefinedDirectlyFollowsGraph = (
|
self.__dfg: RefinedDirectlyFollowsGraph = (
|
||||||
@@ -505,6 +506,27 @@ class TestSelfLoop(unittest.TestCase):
|
|||||||
self.__dfg.self_loops,
|
self.__dfg.self_loops,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_no_self_edge(self) -> None:
|
||||||
|
"""The self-loop edge A->A is not in the edges.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
a: Task = self.__tasks["A"]
|
||||||
|
self.assertNotIn((a, a), self.__dfg.edges)
|
||||||
|
|
||||||
|
def test_other_edges(self) -> None:
|
||||||
|
"""The other edges are A->B and B->C.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
a: Task = self.__tasks["A"]
|
||||||
|
b: Task = self.__tasks["B"]
|
||||||
|
c: Task = self.__tasks["C"]
|
||||||
|
self.assertEqual(
|
||||||
|
self.__dfg.edges,
|
||||||
|
{(a, b), (b, c)},
|
||||||
|
)
|
||||||
|
|
||||||
def test_no_self_loop_in_paper(self) -> None:
|
def test_no_self_loop_in_paper(self) -> None:
|
||||||
"""Paper example has no self-loops.
|
"""Paper example has no self-loops.
|
||||||
|
|
||||||
@@ -514,5 +536,100 @@ class TestSelfLoop(unittest.TestCase):
|
|||||||
self.assertEqual(dfg.self_loops, set())
|
self.assertEqual(dfg.self_loops, set())
|
||||||
|
|
||||||
|
|
||||||
|
class TestSelfLoopWithoutRepeatedEnd(unittest.TestCase):
|
||||||
|
"""Tests a self-loop with a single end event.
|
||||||
|
|
||||||
|
Definition 6 only requires an end event of a followed by
|
||||||
|
a start event of a, not a second end event of a.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Set up trace: As Ae As Bs Be.
|
||||||
|
|
||||||
|
A ends, then A starts again with no end event in
|
||||||
|
between, so A ->r A.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__tasks: dict[str, Task] = (
|
||||||
|
_make_tasks("A", "B")
|
||||||
|
)
|
||||||
|
a: Task = self.__tasks["A"]
|
||||||
|
b: Task = self.__tasks["B"]
|
||||||
|
traces: dict[
|
||||||
|
tuple[tuple[Task, str], ...], int
|
||||||
|
] = {
|
||||||
|
((a, S), (a, E), (a, S),
|
||||||
|
(b, S), (b, E)): 1,
|
||||||
|
}
|
||||||
|
self.__dfg: RefinedDirectlyFollowsGraph = (
|
||||||
|
RefinedDirectlyFollowsGraph(traces)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_is_self_loop(self) -> None:
|
||||||
|
"""A is a self-loop even though it ends once.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.assertIn(
|
||||||
|
self.__tasks["A"],
|
||||||
|
self.__dfg.self_loops,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTwoLoopIsNotSelfLoop(unittest.TestCase):
|
||||||
|
"""Tests that a 2-loop is not mistaken for a self-loop.
|
||||||
|
|
||||||
|
An activity repeating after another activity completes
|
||||||
|
forms a 2-loop, not a self-loop.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Set up trace: As Ae Bs Be As Ae.
|
||||||
|
|
||||||
|
B ends between the end of A and the next start of
|
||||||
|
A, so A ->r A does not hold.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__tasks: dict[str, Task] = (
|
||||||
|
_make_tasks("A", "B")
|
||||||
|
)
|
||||||
|
a: Task = self.__tasks["A"]
|
||||||
|
b: Task = self.__tasks["B"]
|
||||||
|
traces: dict[
|
||||||
|
tuple[tuple[Task, str], ...], int
|
||||||
|
] = {
|
||||||
|
((a, S), (a, E),
|
||||||
|
(b, S), (b, E),
|
||||||
|
(a, S), (a, E)): 1,
|
||||||
|
}
|
||||||
|
self.__dfg: RefinedDirectlyFollowsGraph = (
|
||||||
|
RefinedDirectlyFollowsGraph(traces)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_not_self_loop(self) -> None:
|
||||||
|
"""A is not a self-loop.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.assertNotIn(
|
||||||
|
self.__tasks["A"],
|
||||||
|
self.__dfg.self_loops,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_two_loop_edges(self) -> None:
|
||||||
|
"""The edges form the 2-loop A->B and B->A.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
a: Task = self.__tasks["A"]
|
||||||
|
b: Task = self.__tasks["B"]
|
||||||
|
self.assertEqual(
|
||||||
|
self.__dfg.edges,
|
||||||
|
{(a, b), (b, a)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user