Add split miner implementation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# Split Miner - BPMN process discovery from event logs.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/3/11
|
||||
# AI assistance: Claude Code (Anthropic)
|
||||
|
||||
# Copyright (c) 2026 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# permissions and limitations under the License.
|
||||
"""Tests for the epsilon parameter.
|
||||
|
||||
Ported from the bpmn project's test_epsilon.py.
|
||||
High epsilon values may break the graph.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from split_miner import split_miner
|
||||
|
||||
TRACES_1: dict[tuple[str, ...], int] = {
|
||||
("a", "b", "f", "g", "i", "j", "k"): 1150,
|
||||
("b", "f", "g", "i", "j", "k", "a"): 684,
|
||||
("a", "b", "k"): 432,
|
||||
("a", "b", "k", "f", "g", "i", "j"): 252,
|
||||
("b", "k", "a"): 194,
|
||||
("a", "b", "f", "g", "i", "k", "j"): 192,
|
||||
("a", "h", "f", "g", "i", "j", "k", "b"): 190,
|
||||
("a", "h", "b", "f", "g", "i", "j", "k"): 188,
|
||||
("a", "h", "k", "b"): 80,
|
||||
("a", "h", "b", "k"): 79,
|
||||
("a", "h", "b", "k", "f", "g", "i", "j"): 61,
|
||||
("b", "f", "g", "i", "k", "j", "a"): 53,
|
||||
("a", "h", "b", "f", "g", "i", "k", "j"): 41,
|
||||
("a", "h", "k", "f", "g", "i", "j", "b"): 40,
|
||||
("a", "b", "f", "g", "k", "i", "j"): 36,
|
||||
("a", "h", "f", "g", "i", "k", "j", "b"): 28,
|
||||
("b", "f", "g", "i", "k", "a"): 21,
|
||||
("a", "b", "f", "k", "g", "i", "j"): 19,
|
||||
("a", "f", "g", "i", "j", "k", "h", "b"): 19,
|
||||
("a", "f", "g", "i", "j", "k", "b"): 16,
|
||||
("a", "b", "f", "g", "i", "k"): 15,
|
||||
("a", "h", "b", "f", "g", "k", "i", "j"): 9,
|
||||
("a", "k", "h", "b"): 7,
|
||||
("a", "k", "b"): 6,
|
||||
("a", "b", "k", "f", "g", "i"): 5,
|
||||
("a", "b", "k", "j"): 5,
|
||||
("a", "c", "e", "b", "f", "g", "i", "j",
|
||||
"k"): 5,
|
||||
("a", "h", "f", "g", "i", "j", "k"): 5,
|
||||
("a", "h", "k", "b", "f", "g", "i", "j"): 5,
|
||||
("b", "k", "a", "f", "g", "i", "j"): 5,
|
||||
("a", "b", "j", "k"): 4,
|
||||
("a", "d", "h", "f", "g", "i", "j", "k",
|
||||
"b"): 4,
|
||||
("a", "f", "g", "i", "k", "j", "h", "b"): 4,
|
||||
("a", "h", "f", "g", "k", "i", "j", "b"): 4,
|
||||
("a", "k", "f", "g", "i", "j", "h", "b"): 4,
|
||||
("a", "c", "e", "b", "f", "g", "i", "k",
|
||||
"j"): 3,
|
||||
("a", "c", "e", "b", "k", "f", "g", "i",
|
||||
"j"): 3,
|
||||
("a", "c", "e", "h", "b", "f", "g", "i",
|
||||
"j", "k"): 3,
|
||||
("a", "c", "h", "e", "b", "f", "g", "i",
|
||||
"j", "k"): 3,
|
||||
("a", "d", "h", "b", "k"): 3,
|
||||
("a", "h", "k", "f", "g", "i", "b"): 3,
|
||||
("a", "c", "e", "f", "g", "i", "j", "k",
|
||||
"h", "b"): 2,
|
||||
("a", "c", "e", "h", "f", "g", "i", "j",
|
||||
"k", "b"): 2,
|
||||
("a", "c", "h", "e", "b", "k", "f", "g",
|
||||
"i", "j"): 2,
|
||||
("a", "c", "h", "e", "f", "g", "i", "j",
|
||||
"k", "b"): 2,
|
||||
("a", "d", "b", "f", "g", "i", "j", "k"): 2,
|
||||
("a", "h", "b", "f", "k", "g", "i", "j"): 2,
|
||||
("a", "h", "k"): 2,
|
||||
("b", "f", "g", "i", "k", "a", "j"): 2,
|
||||
("a", "b", "f", "g", "k"): 1,
|
||||
("a", "b", "i", "k"): 1,
|
||||
("a", "c", "e", "b", "k"): 1,
|
||||
("a", "c", "e", "b", "k", "j"): 1,
|
||||
("a", "c", "e", "h", "b", "f", "g", "i",
|
||||
"k"): 1,
|
||||
("a", "c", "e", "h", "b", "k"): 1,
|
||||
("a", "c", "e", "h", "b", "k", "f", "g",
|
||||
"i", "j"): 1,
|
||||
("a", "c", "e", "h", "k", "b"): 1,
|
||||
("a", "c", "h", "e", "b", "k"): 1,
|
||||
("a", "d", "h", "b", "f", "g", "i", "j",
|
||||
"k"): 1,
|
||||
("a", "f", "g", "h", "i", "j", "k", "b"): 1,
|
||||
("a", "f", "g", "i", "b", "j", "k"): 1,
|
||||
("a", "f", "g", "i", "j", "b", "k"): 1,
|
||||
("a", "f", "g", "i", "k", "j", "b"): 1,
|
||||
("a", "f", "g", "i", "k", "j", "h"): 1,
|
||||
("a", "f", "g", "k", "i", "j", "h", "b"): 1,
|
||||
("a", "h", "b", "f", "g", "i", "k"): 1,
|
||||
("a", "h", "b", "f", "g", "k"): 1,
|
||||
("a", "h", "b", "i", "k"): 1,
|
||||
("a", "h", "b", "k", "f", "g", "i"): 1,
|
||||
("a", "h", "b", "k", "j"): 1,
|
||||
("a", "h", "f", "g", "i", "b", "j", "k"): 1,
|
||||
("a", "h", "f", "g", "i", "k"): 1,
|
||||
("a", "h", "f", "g", "i", "k", "b"): 1,
|
||||
("a", "h", "f", "g", "i", "k", "j"): 1,
|
||||
("a", "h", "f", "k", "g", "i", "j", "b"): 1,
|
||||
("a", "h", "j", "k", "b"): 1,
|
||||
("a", "h", "k", "b", "i", "j"): 1,
|
||||
("a", "h", "k", "j", "b"): 1,
|
||||
("a", "k", "b", "f", "g", "i", "j"): 1,
|
||||
("a", "k", "h", "b", "f", "g", "i", "j"): 1,
|
||||
("b", "f", "g", "k", "a"): 1,
|
||||
("b", "i", "j", "k", "a"): 1,
|
||||
("b", "j", "k", "a"): 1,
|
||||
("b", "k", "a", "j"): 1,
|
||||
}
|
||||
"""Traces from the KMU log, filtered and anonymized."""
|
||||
|
||||
TRACES_2: dict[tuple[str, ...], int] = {
|
||||
("a", "g", "e", "c", "d", "f", "h"): 252,
|
||||
("a", "g", "c", "d", "f", "e", "h"): 192,
|
||||
("a", "b", "c", "d", "f", "h", "e", "g"): 190,
|
||||
("a", "g", "c", "d", "e", "f", "h"): 36,
|
||||
("a", "g", "c", "e", "d", "f", "h"): 19,
|
||||
}
|
||||
"""Simplified traces from TRACES_1."""
|
||||
|
||||
TRACES_3: dict[tuple[str, ...], int] = {
|
||||
("a", "b", "d", "b", "e", "d", "c", "e",
|
||||
"d", "e"): 1,
|
||||
("a", "b", "d", "e", "c", "d", "e", "a",
|
||||
"b", "d", "e", "a", "b", "d", "e", "c",
|
||||
"d", "a", "c", "e", "d", "e", "e"): 1,
|
||||
}
|
||||
"""Traces from the rent data."""
|
||||
|
||||
|
||||
class TestEpsilon(unittest.TestCase):
|
||||
"""Tests for the epsilon parameter edge cases."""
|
||||
|
||||
def test_traces_1(self) -> None:
|
||||
"""Tests TRACES_1.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
self.__test_traces(TRACES_1)
|
||||
|
||||
def test_traces_2(self) -> None:
|
||||
"""Tests TRACES_2.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
self.__test_traces(TRACES_2)
|
||||
|
||||
def test_traces_3(self) -> None:
|
||||
"""Tests TRACES_3.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
self.__test_traces(TRACES_3)
|
||||
|
||||
def __test_traces(
|
||||
self,
|
||||
traces: dict[tuple[str, ...], int],
|
||||
) -> None:
|
||||
"""Tests a trace set.
|
||||
|
||||
A high epsilon (0.8) prunes many edges, which can
|
||||
produce a graph with cut vertices. The SPQR-tree
|
||||
construction raises ValueError for non-biconnected
|
||||
graphs, but build_rpst() catches this and falls
|
||||
back to a single fragment. All three epsilon
|
||||
values should succeed without raising.
|
||||
|
||||
:param traces: The traces.
|
||||
:return: None.
|
||||
"""
|
||||
split_miner(traces, epsilon=0.8, eta=0.8)
|
||||
split_miner(traces, epsilon=0.33, eta=0.8)
|
||||
split_miner(traces)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user