Skip to content

API

Datastructures

Defines a vertex/node with a given label and incoming and outgoing edges.

Source code in src/collaboration_detection/datastructures/graph_collection.py
class Vertex:
    """
    Defines a vertex/node with a given label and incoming and outgoing edges.
    """

    def __init__(
        self,
        graph: "Graph",
        vertex_id: int,
        label: str,
        vertex_type: str | None = None,
        metadata: dict[str, Any] | None = None,
        **kwargs,
    ):
        """
        Creates a new vertex inside the graph.

        :param graph: The graph where the vertex is a member
        :param vertex_id: The unique id of the vertex inside the graph
        :param label: The label of the vertex as string
        :param vertex_type: The v_type of the vertex
        :param metadata: Optional metadata for the vertex
        """
        self._graph: Graph = graph
        self._vertex_id = vertex_id
        self.outgoing_edges: list[Edge] = []
        self.incoming_edges: list[Edge] = []
        self._label = label
        self.metadata: dict[str, Any] = {}
        if metadata:
            self.metadata.update(metadata)
        self.metadata.update(**kwargs)
        if vertex_type is None:
            vertex_type = self.metadata.get("v_type", None)
        if vertex_type is None:
            vertex_type = ""
        self.metadata["v_type"] = vertex_type
        _ = self.vertex_type_id
        self._label_id = self.graph.graph_collection.get_set_label_id(self._label, self.metadata.get("v_type", None))
        self.metrics = Vertex._VertexMetrics(self)

    def add_edge(self, other_vertex: "Vertex", directed=True, edge_metadata: dict[str, Any] | None = None) -> "Vertex":
        """
        Add an outgoing edge to this vertex. The other vertex must be present in the same graph.

        :param other_vertex: The other vertex
        :param directed: Defines if the edge is directed or not
        :param edge_metadata: The optional metadata for the edge
        :return: this vertex
        """
        self.graph.add_edge(self, other_vertex, directed=directed, edge_metadata=edge_metadata)
        return self

    @property
    def label(self) -> str:
        """The label property."""
        return self._label

    @label.setter
    def label(self, value):
        self._label = value
        self._label_id = self.graph.graph_collection.get_set_label_id(self._label, self.metadata.get("v_type", None))

    @property
    def vertex_type(self) -> str:
        return self.metadata.get("v_type", "")

    @property
    def vertex_type_id(self) -> int:
        return self.graph.graph_collection.get_set_label_id(self.vertex_type, self.vertex_type)

    @property
    def label_id(self) -> int:
        """The id of the label"""
        return self._label_id

    @property
    def graph(self) -> "Graph":
        """The graph of the vertex"""
        return self._graph

    @property
    def vertex_id(self) -> int:
        """The id of the vertex"""
        return self._vertex_id

    def as_str_rep(self, variant) -> str:
        """
        To string representation:
        The id of the vertex is printed, as well as the label_id as integer and the type id of the vertex
        (No metadata are saved)

        'v id label type'

        e.g.: 'v 1 3 5'
        :return: A string representation of the vertex
        """
        if variant == "subdue":
            return f'v {self.vertex_id + 1} "{self.label}"'
        elif variant == "gspan":
            return f"v {self.vertex_id} {self.label_id}"
        return f"v {self.vertex_id} {self.label_id} {self.vertex_type_id}"

    @property
    def proceeding_vertices(self) -> list["Vertex"]:
        """
        Return all proceeding vertices (from outgoing edges).

        :return: All proceeding vertices (from outgoing edges)
        """
        return [e.to_vertex for e in self.outgoing_edges]

    @property
    def preceding_vertices(self) -> list["Vertex"]:
        """
        Return all preceding vertices (from incoming edges).

        :return: All preceding vertices (from incoming edges)
        """
        return [e.from_vertex for e in self.incoming_edges]

    def get_edge(self, other: "Vertex") -> Optional["Edge"]:
        """
        Get an outgoing edge to the other Vertex, return None if there is no edge

        :param other: The other Vertex
        :return: An edge or None if there is no edge
        """
        m1 = self.graph.edge_mapping.get(self.vertex_id)
        if m1 is None:
            return None
        return m1.get(other.vertex_id)

    def has_edge(self, other: "Vertex") -> bool:
        """
        Check if there is an outgoing edge to the other Vertex

        :param other: The other Vertex
        :return: True if there is an edge, else False
        """
        return self.get_edge(other) is not None

    def __eq__(self, other):
        """
        Two vertices are equal if the vertex_id is equal. The metadata values are not compared.

        :param other: Vertex
        :return: True if both vertices are equal
        """
        if not isinstance(other, Vertex):
            return False
        elif self is other:
            return True
        return self.graph.graph_id == other.graph.graph_id and self.vertex_id == other.vertex_id

    def __str__(self):
        return f"Vertex(#{self.vertex_id}, {self.label} ({self.vertex_type}))"

    def __repr__(self):
        return str(self)

    class _VertexMetrics:
        def __init__(self, vertex: "Vertex"):
            self.vertex = vertex

        @property
        def indegree(self):
            """
            Get the indegree of this vertex = sum of all incoming edges.

            :return: The indegee (int) of this vertex
            """
            return len(self.vertex.incoming_edges)

        @property
        def outdegree(self):
            """
            Get the outdegree of this vertex = sum of all outgoining edges.

            :return: The outdegree (int) of this vertex
            """
            return len(self.vertex.outgoing_edges)

        @property
        def strength(self):
            """
            Get the strength of this vertex = indegree - outdegree

            :return: The strength of this vertex
            """
            return self.indegree - self.outdegree

graph property

The graph of the vertex

label property writable

The label property.

label_id property

The id of the label

preceding_vertices property

Return all preceding vertices (from incoming edges).

:return: All preceding vertices (from incoming edges)

proceeding_vertices property

Return all proceeding vertices (from outgoing edges).

:return: All proceeding vertices (from outgoing edges)

vertex_id property

The id of the vertex

__eq__(other)

Two vertices are equal if the vertex_id is equal. The metadata values are not compared.

:param other: Vertex :return: True if both vertices are equal

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __eq__(self, other):
    """
    Two vertices are equal if the vertex_id is equal. The metadata values are not compared.

    :param other: Vertex
    :return: True if both vertices are equal
    """
    if not isinstance(other, Vertex):
        return False
    elif self is other:
        return True
    return self.graph.graph_id == other.graph.graph_id and self.vertex_id == other.vertex_id

__init__(graph, vertex_id, label, vertex_type=None, metadata=None, **kwargs)

Creates a new vertex inside the graph.

:param graph: The graph where the vertex is a member :param vertex_id: The unique id of the vertex inside the graph :param label: The label of the vertex as string :param vertex_type: The v_type of the vertex :param metadata: Optional metadata for the vertex

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __init__(
    self,
    graph: "Graph",
    vertex_id: int,
    label: str,
    vertex_type: str | None = None,
    metadata: dict[str, Any] | None = None,
    **kwargs,
):
    """
    Creates a new vertex inside the graph.

    :param graph: The graph where the vertex is a member
    :param vertex_id: The unique id of the vertex inside the graph
    :param label: The label of the vertex as string
    :param vertex_type: The v_type of the vertex
    :param metadata: Optional metadata for the vertex
    """
    self._graph: Graph = graph
    self._vertex_id = vertex_id
    self.outgoing_edges: list[Edge] = []
    self.incoming_edges: list[Edge] = []
    self._label = label
    self.metadata: dict[str, Any] = {}
    if metadata:
        self.metadata.update(metadata)
    self.metadata.update(**kwargs)
    if vertex_type is None:
        vertex_type = self.metadata.get("v_type", None)
    if vertex_type is None:
        vertex_type = ""
    self.metadata["v_type"] = vertex_type
    _ = self.vertex_type_id
    self._label_id = self.graph.graph_collection.get_set_label_id(self._label, self.metadata.get("v_type", None))
    self.metrics = Vertex._VertexMetrics(self)

add_edge(other_vertex, directed=True, edge_metadata=None)

Add an outgoing edge to this vertex. The other vertex must be present in the same graph.

:param other_vertex: The other vertex :param directed: Defines if the edge is directed or not :param edge_metadata: The optional metadata for the edge :return: this vertex

Source code in src/collaboration_detection/datastructures/graph_collection.py
def add_edge(self, other_vertex: "Vertex", directed=True, edge_metadata: dict[str, Any] | None = None) -> "Vertex":
    """
    Add an outgoing edge to this vertex. The other vertex must be present in the same graph.

    :param other_vertex: The other vertex
    :param directed: Defines if the edge is directed or not
    :param edge_metadata: The optional metadata for the edge
    :return: this vertex
    """
    self.graph.add_edge(self, other_vertex, directed=directed, edge_metadata=edge_metadata)
    return self

as_str_rep(variant)

To string representation: The id of the vertex is printed, as well as the label_id as integer and the type id of the vertex (No metadata are saved)

'v id label type'

e.g.: 'v 1 3 5' :return: A string representation of the vertex

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_str_rep(self, variant) -> str:
    """
    To string representation:
    The id of the vertex is printed, as well as the label_id as integer and the type id of the vertex
    (No metadata are saved)

    'v id label type'

    e.g.: 'v 1 3 5'
    :return: A string representation of the vertex
    """
    if variant == "subdue":
        return f'v {self.vertex_id + 1} "{self.label}"'
    elif variant == "gspan":
        return f"v {self.vertex_id} {self.label_id}"
    return f"v {self.vertex_id} {self.label_id} {self.vertex_type_id}"

get_edge(other)

Get an outgoing edge to the other Vertex, return None if there is no edge

:param other: The other Vertex :return: An edge or None if there is no edge

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_edge(self, other: "Vertex") -> Optional["Edge"]:
    """
    Get an outgoing edge to the other Vertex, return None if there is no edge

    :param other: The other Vertex
    :return: An edge or None if there is no edge
    """
    m1 = self.graph.edge_mapping.get(self.vertex_id)
    if m1 is None:
        return None
    return m1.get(other.vertex_id)

has_edge(other)

Check if there is an outgoing edge to the other Vertex

:param other: The other Vertex :return: True if there is an edge, else False

Source code in src/collaboration_detection/datastructures/graph_collection.py
def has_edge(self, other: "Vertex") -> bool:
    """
    Check if there is an outgoing edge to the other Vertex

    :param other: The other Vertex
    :return: True if there is an edge, else False
    """
    return self.get_edge(other) is not None

An edge represents a directed arc between two vertices.

Source code in src/collaboration_detection/datastructures/graph_collection.py
class Edge:
    """
    An edge represents a directed arc between two vertices.
    """

    def __init__(
        self,
        graph: "Graph",
        from_vertex: Vertex,
        to_vertex: Vertex,
        metadata: dict[str, Any] | None = None,
        **kwargs,
    ):
        """
        Creates a new directed edge between two vertices.

        :param graph: The graph, where the edge is a member
        :param from_vertex: The source vertex
        :param to_vertex: The sink vertex
        :param metadata: Optional metadata about the edge
        """
        self._graph: Graph = graph
        assert self.graph is from_vertex.graph is to_vertex.graph
        self._from_vertex = from_vertex
        self._from_vertex.outgoing_edges.append(self)
        self._to_vertex = to_vertex
        self._to_vertex.incoming_edges.append(self)
        self.metadata: dict[str, Any] = {}
        if metadata:
            self.metadata.update(metadata)
        self.metadata.update(**kwargs)

    @property
    def graph(self) -> "Graph":
        """The graph of the edge"""
        return self._graph

    @property
    def to_vertex(self) -> Vertex:
        """The sink vertex of this edge"""
        return self._to_vertex

    @property
    def from_vertex(self) -> Vertex:
        """The source vertex of this edge"""
        return self._from_vertex

    def as_str_rep(self, variant="gspan") -> str:
        """
        The str representation of the edge where the ids of the vertices are used.
        (No metadata are saved)

        'e source sink type'

        e.g.:

        'e 1 2 1'

        :return: A string representation of the edge
        """
        if variant == "subdue":
            return f"e {self.from_vertex.vertex_id + 1} {self.to_vertex.vertex_id + 1} 1"
        return f"e {self.from_vertex.vertex_id} {self.to_vertex.vertex_id} 1"

    @property
    def as_edge_rep(self):
        """
        Save as edge representation. (No metadata are saved)

        :return: A tuple of the source vertex label and the sink vertex label, and the type
        """
        return (self.from_vertex.label, self.to_vertex.label), 1

    def __eq__(self, other):
        """
        The other edge is only equal, if the source vertex and the sink vertex are equal.
        The metadata values are not compared.

        :param other: the other edge
        :return: True if both edges are equal
        """
        if not isinstance(other, Edge):
            return False
        elif self is other:
            return True
        return self.from_vertex == other.from_vertex and self.to_vertex == other.to_vertex

    def __str__(self):
        return f"{self.from_vertex} --> {self.to_vertex}"

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash((self.from_vertex.label, self.to_vertex.label))

as_edge_rep property

Save as edge representation. (No metadata are saved)

:return: A tuple of the source vertex label and the sink vertex label, and the type

from_vertex property

The source vertex of this edge

graph property

The graph of the edge

to_vertex property

The sink vertex of this edge

__eq__(other)

The other edge is only equal, if the source vertex and the sink vertex are equal. The metadata values are not compared.

:param other: the other edge :return: True if both edges are equal

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __eq__(self, other):
    """
    The other edge is only equal, if the source vertex and the sink vertex are equal.
    The metadata values are not compared.

    :param other: the other edge
    :return: True if both edges are equal
    """
    if not isinstance(other, Edge):
        return False
    elif self is other:
        return True
    return self.from_vertex == other.from_vertex and self.to_vertex == other.to_vertex

__init__(graph, from_vertex, to_vertex, metadata=None, **kwargs)

Creates a new directed edge between two vertices.

:param graph: The graph, where the edge is a member :param from_vertex: The source vertex :param to_vertex: The sink vertex :param metadata: Optional metadata about the edge

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __init__(
    self,
    graph: "Graph",
    from_vertex: Vertex,
    to_vertex: Vertex,
    metadata: dict[str, Any] | None = None,
    **kwargs,
):
    """
    Creates a new directed edge between two vertices.

    :param graph: The graph, where the edge is a member
    :param from_vertex: The source vertex
    :param to_vertex: The sink vertex
    :param metadata: Optional metadata about the edge
    """
    self._graph: Graph = graph
    assert self.graph is from_vertex.graph is to_vertex.graph
    self._from_vertex = from_vertex
    self._from_vertex.outgoing_edges.append(self)
    self._to_vertex = to_vertex
    self._to_vertex.incoming_edges.append(self)
    self.metadata: dict[str, Any] = {}
    if metadata:
        self.metadata.update(metadata)
    self.metadata.update(**kwargs)

as_str_rep(variant='gspan')

The str representation of the edge where the ids of the vertices are used. (No metadata are saved)

'e source sink type'

e.g.:

'e 1 2 1'

:return: A string representation of the edge

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_str_rep(self, variant="gspan") -> str:
    """
    The str representation of the edge where the ids of the vertices are used.
    (No metadata are saved)

    'e source sink type'

    e.g.:

    'e 1 2 1'

    :return: A string representation of the edge
    """
    if variant == "subdue":
        return f"e {self.from_vertex.vertex_id + 1} {self.to_vertex.vertex_id + 1} 1"
    return f"e {self.from_vertex.vertex_id} {self.to_vertex.vertex_id} 1"
Source code in src/collaboration_detection/datastructures/graph_collection.py
@dataclass
class GraphSetCluster:
    cluster_id: int
    representative: Graph | None
    graphs: set[Graph] = field(default_factory=lambda: set())
    attributes: dict[str, Any] = field(default_factory=dict)

    def zip_images(self, path: str, format: str = "svg"):
        """
        Exports the graphs of a GraphCollection cluster to a zip file (images of the graphs).

        :param path: The base path where the zip file will be saved.
        :param format: The format of the images, Default: svg
        """
        zip_images_of_graphs(list(self.graphs), path, format)

zip_images(path, format='svg')

Exports the graphs of a GraphCollection cluster to a zip file (images of the graphs).

:param path: The base path where the zip file will be saved. :param format: The format of the images, Default: svg

Source code in src/collaboration_detection/datastructures/graph_collection.py
def zip_images(self, path: str, format: str = "svg"):
    """
    Exports the graphs of a GraphCollection cluster to a zip file (images of the graphs).

    :param path: The base path where the zip file will be saved.
    :param format: The format of the images, Default: svg
    """
    zip_images_of_graphs(list(self.graphs), path, format)

A graph represents a set of vertices and a set of edges which connects the vertices.

Source code in src/collaboration_detection/datastructures/graph_collection.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
class Graph:
    """
    A graph represents a set of vertices and a set of edges which connects the vertices.
    """

    def __init__(
        self,
        graph_collection: "GraphCollection",
        graph_id: int,
        cluster_id: int | None = None,
        metadata: dict[str, Any] | None = None,
        **kwargs,
    ):
        """
        Creates a new graph. Each graph is part of a graph collection
        (which contains information about the label mapping)

        :param graph_collection: The Graph Collection
        :param graph_id: The id of the graph
        :param cluster_id: The id of the cluster the graph belongs to
        :param metadata: Optional metadata for the graph
        """
        self.graph_collection = graph_collection
        self.graph_id = graph_id
        self.cluster_id = cluster_id
        self.vertices: dict[int, Vertex] = {}
        self.label_vertices_mapping: dict[int, list[Vertex]] = defaultdict(list)
        self.edges: list[Edge] = []
        self.edge_mapping: dict[int, dict[int, Edge]] = defaultdict(dict)
        self.metadata: dict[str, Any] = {}
        if metadata:
            self.metadata.update(metadata)
        self.metadata.update(**kwargs)

    def add_edge(
        self, from_vertex: Vertex, to_vertex: Vertex, directed=True, edge_metadata: dict[str, Any] | None = None
    ):
        """
        Add a new edge in this graph. Both vertices must be part of this graph.

        :param from_vertex: The source vertex
        :param to_vertex: The sink vertex
        :param directed: if False, the edge is created twice. In the second edge the source and sink vertex are swapped.
        :param edge_metadata: Optional metadata for the edge
        """
        # both, from_vertex and to_vertex should be created first!
        if from_vertex.graph is not self:
            raise ValueError(
                f"From Vertex does not belongs to this graph! "
                f"Was graph {from_vertex.graph.graph_id}; this graph: {self.graph_id}"
            )
        if to_vertex.graph is not self:
            raise ValueError(
                f"To Vertex does not belongs to this graph! "
                f"Was graph {from_vertex.graph.graph_id}; this graph: {self.graph_id}"
            )
        new_edge = Edge(self, from_vertex, to_vertex, metadata=edge_metadata)
        self.edges.append(new_edge)
        self.edge_mapping[from_vertex.vertex_id][to_vertex.vertex_id] = new_edge
        if not directed:
            new_reverse_edge = Edge(self, to_vertex, from_vertex, metadata=edge_metadata)
            self.edges.append(new_reverse_edge)
            self.edge_mapping[to_vertex.vertex_id][from_vertex.vertex_id] = new_reverse_edge

    def get_vertex(
        self,
        label: str,
        *,
        force_create=False,
        vertex_id: int | None = None,
        vertex_type: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> Vertex:
        """
        Get the vertex with the given label. If the vertex does not exist, it will be created.

        :param label: The label of the requested vertex
        :param force_create: Force create a new vertex, even if there is a vertex with the same label
        :param vertex_id: If provided, the vertex with this id is returned if exising, else created with this id.
          If the force create is True, and no vertex_id is provided, a new vertex id will be created.
        :param vertex_type: The optional vertex type of the vertex
        :param metadata: The optional metadata added to the vertex if the vertex is created.
            This parameter will be ignored if the vertex already exists.
        :return: The vertex with the given label
        """
        if vertex_id is not None and vertex_id in self.vertices:
            v = self.get_vertex_by_id(vertex_id)
            if v is not None:
                return v
        if vertex_type is None and metadata is not None:
            vertex_type = metadata.get("v_type", None)
        label_id = self.graph_collection.get_set_label_id(label, vertex_type)
        if not force_create and label_id in self.label_vertices_mapping:
            return self.label_vertices_mapping[label_id][-1]
        if vertex_id is None:
            vertex_id = len(self.vertices)
            while vertex_id in self.vertices:
                vertex_id += 1
        new_vertex = Vertex(self, vertex_id, label, vertex_type=vertex_type, metadata=metadata)
        self.vertices[vertex_id] = new_vertex
        self.label_vertices_mapping[label_id].append(new_vertex)
        return new_vertex

    def get_vertices_by_metadata(self, **kwargs) -> dict[int, Vertex]:
        """
        Get all vertices with the given metadata attributes

        :param kwargs: The attribute key, value pair the vertex metadata must satisfy
        :return: The dict of vertices matching the provided metadata (id: Vertex)
        """
        result = {}
        for v_id, vertex in self.vertices.items():
            accept = True
            for key, value in kwargs.items():
                v_value = vertex.metadata.get(key, None)
                if value != v_value:
                    accept = False
                    break
            if accept:
                result[v_id] = vertex
        return result

    def get_edges_by_metadata(self, **kwargs) -> list[Edge]:
        """
        Get all edges with the given metadata attributes

        :param kwargs: The attribute key, value pair the edge metadata must satisfy
        :return: The dict of edges matching the provided metadata (id: Vertex)
        """
        result = []
        for edge in self.edges:
            accept = True
            for key, value in kwargs.items():
                e_value = edge.metadata.get(key, None)
                if value != e_value:
                    accept = False
                    break
            if accept:
                result.append(edge)

        return result

    def get_edges_by_vertices(self, vertices: Iterable[Vertex]) -> tuple[set[Edge], bool]:
        vertices_ids = {v.vertex_id for v in vertices}
        return self.get_edges_by_vertices_ids(vertices_ids)

    def get_edges_by_vertices_ids(self, vertices: Iterable[int]) -> tuple[set[Edge], bool]:
        result = set()
        vertices = set(vertices)
        visited_vertices = set()
        for v_id in vertices:
            for v_e_id, edge in self.edge_mapping[v_id].items():
                if v_e_id in vertices:
                    visited_vertices.add(v_id)
                    visited_vertices.add(v_e_id)
                    result.add(edge)
        return result, len(vertices) == len(visited_vertices)

    def get_vertex_by_id(self, vertex_id: int) -> Vertex | None:
        """
        Get the vertex with the given vertex_id. If the vertex does not exist, the function will return None.

        :param vertex_id: The label of the requested vertex
        :return: The vertex with the given vertex_id or None, if the vertex does not exist.
        """
        return self.vertices.get(vertex_id, None)

    def copy_into(
        self, graph_collection: "GraphCollection", additional_metadata: dict[str, Any] | None = None
    ) -> "Graph":
        """
        Copy this graph into a graph_collection

        :param graph_collection: The other graph collection
        :return: The new graph on the other graph_collection
        """
        metadata = self.metadata.copy()
        if additional_metadata is not None:
            metadata.update(additional_metadata)
        new_graph = graph_collection.new_graph(metadata=metadata)
        for v_id, v in self.vertices.items():
            new_graph.get_vertex(v.label, force_create=True, vertex_id=v_id, metadata=v.metadata.copy())
        for e in self.edges:
            new_graph.add_edge(
                new_graph.get_vertex_by_id(e.from_vertex.vertex_id),
                new_graph.get_vertex_by_id(e.to_vertex.vertex_id),
                edge_metadata=e.metadata.copy(),
            )
        return new_graph

    def as_str_rep(self, variant="gspan") -> str:
        """
        Returns a string of the graph with all vertices and edges.
        (No metadata are saved)

         - t # graph_id
         - v vertex_id vertex_label_id (vertex_type_id)
         - v vertex_id vertex_label_id (vertex_type_id)
         - ...
         - e vertex_source_id vertex_sink_id edge_type
         - e vertex_source_id vertex_sink_id edge_type
         - ...

        :return: The string representation of the graph
        """
        lines = [f"t # {self.graph_id}"]
        if variant == "subdue":
            lines = ["XP"]
        lines.extend(v.as_str_rep(variant) for _, v in self.vertices.items())
        lines.extend(e.as_str_rep(variant) for e in self.edges)
        return "\n".join(lines)

    @property
    def as_edge_rep(self) -> dict:
        """
        Returns a dict of the graph with all edges.
        The keys are tuples of strings (the two connected vertices) The vertices are defined by their label (string).
        The value is the type of the edge. (No metadata are saved)

        e.g.

        {('A','B'):2,('B','C'):1}

        :return: The edge representation of a graph
        """
        return {e[0]: e[1] for e in (x.as_edge_rep for x in self.edges)}

    @property
    def as_edge_tuple(self) -> list[tuple]:
        """
        Returns a dict of the graph with all edges.
        The keys are tuples of strings (the two connected vertices) The vertices are defined by their label (string).
        The value is the type of the edge.
        e.g.

        {('A','B'):2,('B','C'):1}

        :return: The edge representation of a graph
        """
        return [e[0] for e in (x.as_edge_rep for x in self.edges)]

    @property
    def as_networkx_digraph(self) -> nx.DiGraph:
        """
        Converts the graph into a python networkx graph object
        :return: DiGraph object of this graph
        """
        dg = nx.DiGraph()
        for v in self.vertices.values():
            dg.add_node(v.vertex_id, label=v.label, label_id=v.label_id, vertex_type_id=v.vertex_type_id, **v.metadata)
        dg.add_edges_from((e.from_vertex.vertex_id, e.to_vertex.vertex_id, e.metadata) for e in self.edges)
        dg.graph_id = self.graph_id
        return dg

    @property
    def as_graph_map(self) -> GraphMap:
        """
        Converts the graph into a graph map. (No metadata are saved)
        :return: A Graph Map
        """
        vertices = {str(vertex_id): MapVertex(vertex.label, vertex_id) for vertex_id, vertex in self.vertices.items()}
        edges = {
            f"{edge.from_vertex.vertex_id}:{edge.to_vertex.vertex_id}": MapEdge(
                vertices[str(edge.from_vertex.vertex_id)], vertices[str(edge.to_vertex.vertex_id)]
            )
            for edge in self.edges
        }
        return GraphMap(vertices, edges, str(self.graph_id))

    def to_dot_digraph(self, path: str | None = None, rankdir: str = "TB") -> Digraph:
        """
        Create a digraph object of the graph.

        :param path: if filled with a path to a file name (.png/.svg) the digraph is saved to the given path
        :param rankdir: the rank direction of the graph, "TB" or "LR"
        :return: a digraph
        """
        with tempfile.NamedTemporaryFile(suffix=".gv", delete=False) as tmp_file:
            tmp_file_name = tmp_file.name
        viz = Digraph(
            "",
            filename=tmp_file_name,
            engine="dot",
            graph_attr={"bgcolor": "transparent", "rankdir": rankdir},
        )
        viz.format = "svg"
        if path:
            viz.format = path.split(".")[-1]
        # draw nodes
        for v in self.vertices.values():
            bg = v.metadata.get("fillcolor", None)
            if bg is None:
                if v.vertex_type == "activity" or v.vertex_type == "":
                    bg = "#800000"
                else:
                    bg = "#000088"

            fc = v.metadata.get("fontcolor") or get_font_color(background_color=bg)
            shape = v.metadata.get("shape", None)
            if shape is None:
                if v.vertex_type == "activity" or v.vertex_type == "":
                    shape = "box"
                else:
                    shape = "oval"

            rank = v.metadata.get("rank")
            viz.node(
                str(v.vertex_id),
                v.label,
                shape=shape,
                style="filled",
                fontcolor=fc,
                fillcolor=bg,
                rank=rank,
                fontsize=str(12),
            )

        # draw edges
        for edge in self.edges:
            label = edge.metadata.get("label", "")
            dir_value = "none"
            penwidth = edge.metadata.get("penwidth", "0.8")
            style = "dashed"
            if (
                not edge.metadata.get("undirected", True)
                or (edge.from_vertex.vertex_type == "activity" or edge.from_vertex.vertex_type == "")
                and (edge.to_vertex.vertex_type == "activity" or edge.to_vertex.vertex_type == "")
            ):
                dir_value = "forward"
                penwidth = edge.metadata.get("penwidth", "1.2")
                style = ""
            viz.edge(
                str(edge.from_vertex.vertex_id),
                str(edge.to_vertex.vertex_id),
                label=label,
                penwidth=penwidth,
                fontsize=str(12),
                dir=dir_value,
                style=style,
            )
        viz.attr(overlap="false")
        viz.render(tmp_file.name, outfile=path, cleanup=True)
        return viz

    def _repr_svg_(self):
        dot = self.to_dot_digraph()
        data = dot.pipe(format="svg")
        svg_str = data.decode("utf-8")
        return svg_str

    @property
    def as_adjacency_matrix(self) -> pd.DataFrame:
        """
        Create an adjacency matrix of this graph as Pandas DataFrame.

        :return: the adjacency matrix of the graph as Pandas DataFrame
        """
        v_l = [v_id for v_id, v in sorted(self.vertices.items())]
        am = pd.DataFrame(np.zeros(shape=(len(v_l), len(v_l)), dtype=int), columns=v_l, index=v_l)
        for e in self.edges:
            am.at[e.from_vertex.vertex_id, e.to_vertex.vertex_id] += 1
        return am

    def unlink(self) -> "Graph":
        self.graph_collection = None
        return self

    def is_subgraph_of(self, other: "Graph") -> bool:
        """
        Check if the current graph is a subgraph of the other graph.
        A graph is a subgraph, only if all its vertices are in the other graph
        and all its edges are also in the other graph.

        :param other: The other graph (the super graph)
        :return: True if the current graph is a subgraph of the other graph.
        """
        # TODO: DOES NOT WORK!
        return all(v_s in other.vertices for v_s in self.vertices) and all(e_s in other.edges for e_s in self.edges)

    def __str__(self):
        return f"Graph ({self.graph_id}) with {len(self.vertices)} vertices and {len(self.edges)} edges"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        """
        A graph is equal to another graph if the other graph hase the same edges and vertices.
        The metadata are not compared.

        :param other: The other graph
        :return: True if the graphs are equal
        """
        if not isinstance(other, Graph):
            return False
        elif self is other:
            return True
        else:
            return len(self.vertices) == len(other.vertices) and set(self.edges) == set(other.edges)

    def __len__(self):
        """
        The len of a graph is equal to len(graph.vertices)

        :return: The len of the graph
        """
        return len(self.vertices)

    def __hash__(self):
        return hash(self.graph_id)

as_adjacency_matrix property

Create an adjacency matrix of this graph as Pandas DataFrame.

:return: the adjacency matrix of the graph as Pandas DataFrame

as_edge_rep property

Returns a dict of the graph with all edges. The keys are tuples of strings (the two connected vertices) The vertices are defined by their label (string). The value is the type of the edge. (No metadata are saved)

e.g.

{('A','B'):2,('B','C'):1}

:return: The edge representation of a graph

as_edge_tuple property

Returns a dict of the graph with all edges. The keys are tuples of strings (the two connected vertices) The vertices are defined by their label (string). The value is the type of the edge. e.g.

{('A','B'):2,('B','C'):1}

:return: The edge representation of a graph

as_graph_map property

Converts the graph into a graph map. (No metadata are saved) :return: A Graph Map

as_networkx_digraph property

Converts the graph into a python networkx graph object :return: DiGraph object of this graph

__eq__(other)

A graph is equal to another graph if the other graph hase the same edges and vertices. The metadata are not compared.

:param other: The other graph :return: True if the graphs are equal

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __eq__(self, other):
    """
    A graph is equal to another graph if the other graph hase the same edges and vertices.
    The metadata are not compared.

    :param other: The other graph
    :return: True if the graphs are equal
    """
    if not isinstance(other, Graph):
        return False
    elif self is other:
        return True
    else:
        return len(self.vertices) == len(other.vertices) and set(self.edges) == set(other.edges)

__init__(graph_collection, graph_id, cluster_id=None, metadata=None, **kwargs)

Creates a new graph. Each graph is part of a graph collection (which contains information about the label mapping)

:param graph_collection: The Graph Collection :param graph_id: The id of the graph :param cluster_id: The id of the cluster the graph belongs to :param metadata: Optional metadata for the graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __init__(
    self,
    graph_collection: "GraphCollection",
    graph_id: int,
    cluster_id: int | None = None,
    metadata: dict[str, Any] | None = None,
    **kwargs,
):
    """
    Creates a new graph. Each graph is part of a graph collection
    (which contains information about the label mapping)

    :param graph_collection: The Graph Collection
    :param graph_id: The id of the graph
    :param cluster_id: The id of the cluster the graph belongs to
    :param metadata: Optional metadata for the graph
    """
    self.graph_collection = graph_collection
    self.graph_id = graph_id
    self.cluster_id = cluster_id
    self.vertices: dict[int, Vertex] = {}
    self.label_vertices_mapping: dict[int, list[Vertex]] = defaultdict(list)
    self.edges: list[Edge] = []
    self.edge_mapping: dict[int, dict[int, Edge]] = defaultdict(dict)
    self.metadata: dict[str, Any] = {}
    if metadata:
        self.metadata.update(metadata)
    self.metadata.update(**kwargs)

__len__()

The len of a graph is equal to len(graph.vertices)

:return: The len of the graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __len__(self):
    """
    The len of a graph is equal to len(graph.vertices)

    :return: The len of the graph
    """
    return len(self.vertices)

add_edge(from_vertex, to_vertex, directed=True, edge_metadata=None)

Add a new edge in this graph. Both vertices must be part of this graph.

:param from_vertex: The source vertex :param to_vertex: The sink vertex :param directed: if False, the edge is created twice. In the second edge the source and sink vertex are swapped. :param edge_metadata: Optional metadata for the edge

Source code in src/collaboration_detection/datastructures/graph_collection.py
def add_edge(
    self, from_vertex: Vertex, to_vertex: Vertex, directed=True, edge_metadata: dict[str, Any] | None = None
):
    """
    Add a new edge in this graph. Both vertices must be part of this graph.

    :param from_vertex: The source vertex
    :param to_vertex: The sink vertex
    :param directed: if False, the edge is created twice. In the second edge the source and sink vertex are swapped.
    :param edge_metadata: Optional metadata for the edge
    """
    # both, from_vertex and to_vertex should be created first!
    if from_vertex.graph is not self:
        raise ValueError(
            f"From Vertex does not belongs to this graph! "
            f"Was graph {from_vertex.graph.graph_id}; this graph: {self.graph_id}"
        )
    if to_vertex.graph is not self:
        raise ValueError(
            f"To Vertex does not belongs to this graph! "
            f"Was graph {from_vertex.graph.graph_id}; this graph: {self.graph_id}"
        )
    new_edge = Edge(self, from_vertex, to_vertex, metadata=edge_metadata)
    self.edges.append(new_edge)
    self.edge_mapping[from_vertex.vertex_id][to_vertex.vertex_id] = new_edge
    if not directed:
        new_reverse_edge = Edge(self, to_vertex, from_vertex, metadata=edge_metadata)
        self.edges.append(new_reverse_edge)
        self.edge_mapping[to_vertex.vertex_id][from_vertex.vertex_id] = new_reverse_edge

as_str_rep(variant='gspan')

Returns a string of the graph with all vertices and edges. (No metadata are saved)

  • t # graph_id
  • v vertex_id vertex_label_id (vertex_type_id)
  • v vertex_id vertex_label_id (vertex_type_id)
  • ...
  • e vertex_source_id vertex_sink_id edge_type
  • e vertex_source_id vertex_sink_id edge_type
  • ...

:return: The string representation of the graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_str_rep(self, variant="gspan") -> str:
    """
    Returns a string of the graph with all vertices and edges.
    (No metadata are saved)

     - t # graph_id
     - v vertex_id vertex_label_id (vertex_type_id)
     - v vertex_id vertex_label_id (vertex_type_id)
     - ...
     - e vertex_source_id vertex_sink_id edge_type
     - e vertex_source_id vertex_sink_id edge_type
     - ...

    :return: The string representation of the graph
    """
    lines = [f"t # {self.graph_id}"]
    if variant == "subdue":
        lines = ["XP"]
    lines.extend(v.as_str_rep(variant) for _, v in self.vertices.items())
    lines.extend(e.as_str_rep(variant) for e in self.edges)
    return "\n".join(lines)

copy_into(graph_collection, additional_metadata=None)

Copy this graph into a graph_collection

:param graph_collection: The other graph collection :return: The new graph on the other graph_collection

Source code in src/collaboration_detection/datastructures/graph_collection.py
def copy_into(
    self, graph_collection: "GraphCollection", additional_metadata: dict[str, Any] | None = None
) -> "Graph":
    """
    Copy this graph into a graph_collection

    :param graph_collection: The other graph collection
    :return: The new graph on the other graph_collection
    """
    metadata = self.metadata.copy()
    if additional_metadata is not None:
        metadata.update(additional_metadata)
    new_graph = graph_collection.new_graph(metadata=metadata)
    for v_id, v in self.vertices.items():
        new_graph.get_vertex(v.label, force_create=True, vertex_id=v_id, metadata=v.metadata.copy())
    for e in self.edges:
        new_graph.add_edge(
            new_graph.get_vertex_by_id(e.from_vertex.vertex_id),
            new_graph.get_vertex_by_id(e.to_vertex.vertex_id),
            edge_metadata=e.metadata.copy(),
        )
    return new_graph

get_edges_by_metadata(**kwargs)

Get all edges with the given metadata attributes

:param kwargs: The attribute key, value pair the edge metadata must satisfy :return: The dict of edges matching the provided metadata (id: Vertex)

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_edges_by_metadata(self, **kwargs) -> list[Edge]:
    """
    Get all edges with the given metadata attributes

    :param kwargs: The attribute key, value pair the edge metadata must satisfy
    :return: The dict of edges matching the provided metadata (id: Vertex)
    """
    result = []
    for edge in self.edges:
        accept = True
        for key, value in kwargs.items():
            e_value = edge.metadata.get(key, None)
            if value != e_value:
                accept = False
                break
        if accept:
            result.append(edge)

    return result

get_vertex(label, *, force_create=False, vertex_id=None, vertex_type=None, metadata=None)

Get the vertex with the given label. If the vertex does not exist, it will be created.

:param label: The label of the requested vertex :param force_create: Force create a new vertex, even if there is a vertex with the same label :param vertex_id: If provided, the vertex with this id is returned if exising, else created with this id. If the force create is True, and no vertex_id is provided, a new vertex id will be created. :param vertex_type: The optional vertex type of the vertex :param metadata: The optional metadata added to the vertex if the vertex is created. This parameter will be ignored if the vertex already exists. :return: The vertex with the given label

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_vertex(
    self,
    label: str,
    *,
    force_create=False,
    vertex_id: int | None = None,
    vertex_type: str | None = None,
    metadata: dict[str, Any] | None = None,
) -> Vertex:
    """
    Get the vertex with the given label. If the vertex does not exist, it will be created.

    :param label: The label of the requested vertex
    :param force_create: Force create a new vertex, even if there is a vertex with the same label
    :param vertex_id: If provided, the vertex with this id is returned if exising, else created with this id.
      If the force create is True, and no vertex_id is provided, a new vertex id will be created.
    :param vertex_type: The optional vertex type of the vertex
    :param metadata: The optional metadata added to the vertex if the vertex is created.
        This parameter will be ignored if the vertex already exists.
    :return: The vertex with the given label
    """
    if vertex_id is not None and vertex_id in self.vertices:
        v = self.get_vertex_by_id(vertex_id)
        if v is not None:
            return v
    if vertex_type is None and metadata is not None:
        vertex_type = metadata.get("v_type", None)
    label_id = self.graph_collection.get_set_label_id(label, vertex_type)
    if not force_create and label_id in self.label_vertices_mapping:
        return self.label_vertices_mapping[label_id][-1]
    if vertex_id is None:
        vertex_id = len(self.vertices)
        while vertex_id in self.vertices:
            vertex_id += 1
    new_vertex = Vertex(self, vertex_id, label, vertex_type=vertex_type, metadata=metadata)
    self.vertices[vertex_id] = new_vertex
    self.label_vertices_mapping[label_id].append(new_vertex)
    return new_vertex

get_vertex_by_id(vertex_id)

Get the vertex with the given vertex_id. If the vertex does not exist, the function will return None.

:param vertex_id: The label of the requested vertex :return: The vertex with the given vertex_id or None, if the vertex does not exist.

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_vertex_by_id(self, vertex_id: int) -> Vertex | None:
    """
    Get the vertex with the given vertex_id. If the vertex does not exist, the function will return None.

    :param vertex_id: The label of the requested vertex
    :return: The vertex with the given vertex_id or None, if the vertex does not exist.
    """
    return self.vertices.get(vertex_id, None)

get_vertices_by_metadata(**kwargs)

Get all vertices with the given metadata attributes

:param kwargs: The attribute key, value pair the vertex metadata must satisfy :return: The dict of vertices matching the provided metadata (id: Vertex)

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_vertices_by_metadata(self, **kwargs) -> dict[int, Vertex]:
    """
    Get all vertices with the given metadata attributes

    :param kwargs: The attribute key, value pair the vertex metadata must satisfy
    :return: The dict of vertices matching the provided metadata (id: Vertex)
    """
    result = {}
    for v_id, vertex in self.vertices.items():
        accept = True
        for key, value in kwargs.items():
            v_value = vertex.metadata.get(key, None)
            if value != v_value:
                accept = False
                break
        if accept:
            result[v_id] = vertex
    return result

is_subgraph_of(other)

Check if the current graph is a subgraph of the other graph. A graph is a subgraph, only if all its vertices are in the other graph and all its edges are also in the other graph.

:param other: The other graph (the super graph) :return: True if the current graph is a subgraph of the other graph.

Source code in src/collaboration_detection/datastructures/graph_collection.py
def is_subgraph_of(self, other: "Graph") -> bool:
    """
    Check if the current graph is a subgraph of the other graph.
    A graph is a subgraph, only if all its vertices are in the other graph
    and all its edges are also in the other graph.

    :param other: The other graph (the super graph)
    :return: True if the current graph is a subgraph of the other graph.
    """
    # TODO: DOES NOT WORK!
    return all(v_s in other.vertices for v_s in self.vertices) and all(e_s in other.edges for e_s in self.edges)

to_dot_digraph(path=None, rankdir='TB')

Create a digraph object of the graph.

:param path: if filled with a path to a file name (.png/.svg) the digraph is saved to the given path :param rankdir: the rank direction of the graph, "TB" or "LR" :return: a digraph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def to_dot_digraph(self, path: str | None = None, rankdir: str = "TB") -> Digraph:
    """
    Create a digraph object of the graph.

    :param path: if filled with a path to a file name (.png/.svg) the digraph is saved to the given path
    :param rankdir: the rank direction of the graph, "TB" or "LR"
    :return: a digraph
    """
    with tempfile.NamedTemporaryFile(suffix=".gv", delete=False) as tmp_file:
        tmp_file_name = tmp_file.name
    viz = Digraph(
        "",
        filename=tmp_file_name,
        engine="dot",
        graph_attr={"bgcolor": "transparent", "rankdir": rankdir},
    )
    viz.format = "svg"
    if path:
        viz.format = path.split(".")[-1]
    # draw nodes
    for v in self.vertices.values():
        bg = v.metadata.get("fillcolor", None)
        if bg is None:
            if v.vertex_type == "activity" or v.vertex_type == "":
                bg = "#800000"
            else:
                bg = "#000088"

        fc = v.metadata.get("fontcolor") or get_font_color(background_color=bg)
        shape = v.metadata.get("shape", None)
        if shape is None:
            if v.vertex_type == "activity" or v.vertex_type == "":
                shape = "box"
            else:
                shape = "oval"

        rank = v.metadata.get("rank")
        viz.node(
            str(v.vertex_id),
            v.label,
            shape=shape,
            style="filled",
            fontcolor=fc,
            fillcolor=bg,
            rank=rank,
            fontsize=str(12),
        )

    # draw edges
    for edge in self.edges:
        label = edge.metadata.get("label", "")
        dir_value = "none"
        penwidth = edge.metadata.get("penwidth", "0.8")
        style = "dashed"
        if (
            not edge.metadata.get("undirected", True)
            or (edge.from_vertex.vertex_type == "activity" or edge.from_vertex.vertex_type == "")
            and (edge.to_vertex.vertex_type == "activity" or edge.to_vertex.vertex_type == "")
        ):
            dir_value = "forward"
            penwidth = edge.metadata.get("penwidth", "1.2")
            style = ""
        viz.edge(
            str(edge.from_vertex.vertex_id),
            str(edge.to_vertex.vertex_id),
            label=label,
            penwidth=penwidth,
            fontsize=str(12),
            dir=dir_value,
            style=style,
        )
    viz.attr(overlap="false")
    viz.render(tmp_file.name, outfile=path, cleanup=True)
    return viz

A GraphCollection represents a collection of graphs. It holds these graphs in a list, where each graph has a unique ID. It further contains a shared mapping for the labels.

Source code in src/collaboration_detection/datastructures/graph_collection.py
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
class GraphCollection:
    """
    A GraphCollection represents a collection of graphs.
    It holds these graphs in a list, where each graph has a unique ID.
    It further contains a shared mapping for the labels.
    """

    def __init__(self, *, label_list: list[tuple[str, str]] | str | None = None, collection_id: str | None = None):
        """
        Creates a new GraphCollection.

        :param label_list: The label_list as list or path to a pickel file containing the label_list,
        optional, can be created on the fly by building the graph_collection
        :param collection_id: An id for this collection, if None, a new unique id one will be created
        """

        self.label_mapping: dict[str, int] = {}
        self.label_list: list[tuple[str, str]] = []
        self.graphs: dict[int, Graph] = {}
        self.init_label_mapping(label_list)
        self.clusters: dict[int, GraphSetCluster] = {}
        self._collection_id: str = collection_id if collection_id is not None else str(uuid.uuid4())

    @property
    def collection_id(self):
        """
        Get the collection id of this collection
        """
        return self._collection_id

    def new_graph(
        self,
        *,
        graph_id: int | None = None,
        cluster_id: int | None = None,
        metadata: dict | None = None,
        **kwargs,
    ) -> Graph:
        """
        Creates a new graph in this graph collection. The graph gets a new unique id.

        :param graph_id: An id for the graph
        :param cluster_id: An id for the cluster the graph belongs to
        :param metadata: Optional additional metadata as dict.
        :return: a new Graph object
        """
        graph_id = graph_id if graph_id is not None else len(self.graphs)
        g = Graph(self, graph_id, cluster_id=cluster_id, metadata=metadata, **kwargs)
        self.graphs[graph_id] = g
        return g

    def filter(
        self,
        vertex_count_min: int | None = None,
        vertex_count_max: int | None = None,
        edge_count_min: int | None = None,
        edge_count_max: int | None = None,
        vertex_label_in: str | None = None,
        vertex_label_is: str | None = None,
        cluster_id: int | None = None,
        expected_metadata: dict[str, Any] | None = None,
        filter_func: Callable[[Graph], bool] | None = None,
    ) -> Iterator[Graph]:
        """
        Creates an iterator for this graph collection.
        It iterates over all graphs and applies the given filter.

        :param vertex_count_min: The graph should contain at least x vertices
        :param vertex_count_max: The graph should contain at maximum x vertices
        :param edge_count_min: The graph should contain at least x edges
        :param edge_count_max: The graph should contain at maximum x edges
        :param vertex_label_in: The graph should contain a vertex with a label matches a part of this value
        :param vertex_label_is: The graph should contain a vertex with a label matches this value
        :param cluster_id: The graph should be part of the given cluster_id
        :param expected_metadata: The graph should have the values of the specified expected_metadata
        :param filter_func: A filter function that gets a graph and should return true or false
        :return: Iterator over the graphs
        """
        graphs: GraphCollection | set[Graph] = self
        if cluster_id is not None:
            cluster = self.clusters.get(cluster_id)
            graphs = cluster.graphs if cluster is not None else set()
        for g in graphs:
            if vertex_count_min is not None and len(g.vertices) < vertex_count_min:
                continue
            if vertex_count_max is not None and len(g.vertices) > vertex_count_max:
                continue
            if edge_count_min is not None and len(g.edges) < edge_count_min:
                continue
            if edge_count_max is not None and len(g.edges) > edge_count_max:
                continue
            if vertex_label_in is not None and not any(vertex_label_in in v.label for v_id, v in g.vertices.items()):
                continue
            if vertex_label_is is not None and not any(vertex_label_is == v.label for v_id, v in g.vertices.items()):
                continue
            if cluster_id is not None and g.cluster_id == cluster_id:
                continue
            if filter_func is not None and not filter_func(g):
                continue
            if expected_metadata is not None and not all(
                g.metadata.get(key, None) == exp_value for key, exp_value in expected_metadata.items()
            ):
                continue

            yield g

    def clean(self, label_list: list[tuple[str, str]] | str | None = None):
        """
        Clean the graph collection. Optional initialize the label mapping with the given label_list.
        The label_list should contain strings of the labels.

        :param label_list: The list of labels. Or path to the pickle file.
        """
        self.graphs = {}
        self.init_label_mapping(label_list)
        self.clusters = {}

    def init_label_mapping(self, label_list: list[tuple[str, str]] | str | None = None, format="pickle"):
        """
        Init the label mapping with the given list of label tuples.
        The index of the label is the id of the label mapping.
        The param label_list can be a path to a pickle file which was created with 'save_label_mapping'.

        :param label_list: The list of labels. Or path to the pickle file or text file.
        :param format: The format of the file (pickle or text), if label_list is the path to the file.
        """
        # dict {('a'):1,('b'):0}
        self.label_mapping = {}
        if label_list is None:
            # list [('b'),('a')]
            self.label_list = []
        elif isinstance(label_list, str):
            with open(label_list, "rb" if format == "pickle" else "r") as file:
                if format == "pickle":
                    self.label_list = pickle.load(file)
                elif format == "text":
                    self.label_list = [tuple(line.strip().split(";;;")) for line in file]
        else:
            self.label_list = deepcopy(label_list)
        for i, (val, _) in enumerate(self.label_list):
            self.label_mapping[val] = i

    def save_label_mapping(self, file_path, format="pickle"):
        """
        Save the label mapping in a pickle file

        :param file_path: the file_path of the output
        :param format: "pickle" if pickle should be used, "text" plain if a text file should be used
        """
        with open(file_path, "wb" if format == "pickle" else "w") as file:
            if format == "pickle":
                pickle.dump(self.label_list, file=file)
            elif format == "text":
                file.write(os.linesep.join(f"{label};;;{l_type}" for (label, l_type) in self.label_list))

    def get_label(self, label_id: int) -> str | None:
        """
        Get the label for the given label id

        :param label_id: the id of the label
        :return: the label as string or None if the id is not present
        """
        if len(self.label_list) <= label_id:
            return None
        return self.label_list[label_id][0]

    def get_label_type(self, label_id: int) -> str | None:
        """
        Get the label type for the given label id

        :param label_id: the id of the label
        :return: the label type as string or None if the id or none if label type is None for the label id is not present
        """
        if len(self.label_list) <= label_id:
            return None
        return self.label_list[label_id][1]

    def get_label_type_by_label(self, label: str) -> str | None:
        """
        Get the label type for the given label

        :param label: the label as string
        :return: the label type as string or None if the id or none if label type is None for the label id is not present
        """
        label_id = self.get_set_label_id(label)
        return self.label_list[label_id][1]

    def get_set_label_id(self, label: str, label_type: str | None = None) -> int:
        """
        Get the id of the given label. If the label is not present, it is created for this graph_collection.

        :param label: the label as string
        :return: the id of the label
        """
        label = label.strip()
        if label_type is None:
            label_type = ""
        if label_type != "" and label != label_type:
            self.get_set_label_id(label_type, label_type)
        label_id = self.label_mapping.get(label, None)
        if label_id is not None:
            return label_id
        else:
            new_id = len(self.label_list)
            self.label_list.append((label, label_type))
            self.label_mapping[label] = new_id
            return new_id

    def export(self, path: str):
        """
        Saves the graph collection as pickle file.

        :file: the path of the file
        """
        with open(path, "wb") as f:
            pickle.dump(self, f, pickle.HIGHEST_PROTOCOL)

    @staticmethod
    def load(path: str) -> "GraphCollection":
        """
        Loads the graph collection from a pickle file

        :file: the path of the file
        """
        with open(path, "rb") as f:
            model: GraphCollection = pickle.load(f)
            return model

    def as_str_rep(self, variant="gspan") -> str:
        """
        Creates the string representation of the graph collection.
        Important: The labels are printed as integer values (the id of the label)
        The variant can be 'gspan' or 'subdue'.

        - t # graph_id
        - v vertex_id vertex_attributes_id
        - v vertex_id vertex_attributes_id
        - ...
        - e vertex_source_id vertex_sink_id edge_type
        - e vertex_source_id vertex_sink_id edge_type
        - ...
        - t # graph_id
        - ...

        :return: The string representation of the graphs
        """
        return "\n".join(self.as_str_rep_lines(variant))

    def as_str_rep_lines(self, variant="gspan") -> list[str]:
        """
        Creates the string representation of the graph collection.
        Important: The labels are printed as integer values (the id of the label)
        The variant can be 'gspan' or 'subdue'.

        :return: a list of strings (the string representation of the graphs)
        """
        return [g.as_str_rep(variant) for g in self]

    def save_str_rep(self, file, variant="gspan"):
        """
        Saves the graph collection as string representation as file.
        The variant can be 'gspan' or 'subdue'.

        :param file: The path or the file object
        """
        if isinstance(file, str):
            with open(file, "w") as f:
                f.write(self.as_str_rep(variant))
                f.flush()
        else:
            file.write(self.as_str_rep(variant))
            file.flush()

    def as_edge_rep(self) -> list[dict]:
        """
        Creates an edge representation of the graphs.
        So the output of this function is a list of graphs.
        Each graph is represented as a dict.
        Each Edge is represented as a tuple of two strings, which are the both connected vertices.
        The value represents the type of the edge.

        [{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

        :return: a list of dicts (graphs as edge representation)
        """
        return [g.as_edge_rep for g in self]

    def as_networkx_digraphs(self) -> list[nx.DiGraph]:
        """
        Creates a list of networkx.DiGraph objects.

        :return: A list of networkx.DiGraph objects of the graphs
        """
        return [g.as_networkx_digraph for g in self]

    def as_graph_maps(self) -> list[GraphMap]:
        return [g.as_graph_map for g in self]

    def tar_images(self, path: str, format: str = "svg"):
        """
        Exports the graphs of a GraphCollection to a tar file (images of the graphs).

        :param path: The base path where the tar file will be saved.
        :param format: The format of the images, Default: svg
        """
        tar_images_of_g_c(self, path, format)

    def zip_images(self, path: str, format: str = "svg"):
        """
        Exports the graphs of a GraphCollection to a zip file (images of the graphs).

        :param path: The base path where the zip file will be saved.
        :param format: The format of the images, Default: svg
        """
        zip_images_of_g_c(self, path, format)

    def load_graphs_from_edge_rep(
        self,
        graph_edge_rep: dict[tuple[str, str], int],
        ignore_type=True,
    ) -> Graph:
        """
        Loads the graph collection from an edge representation.

        e.g.  [{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

        :param graph_edge_rep: The edge representation object
        :param ignore_type: Ignores the edge type (sets to 1)
        :return: the created graph
        """
        new_graph = self.new_graph()
        for l1, l2 in graph_edge_rep:
            v1 = new_graph.get_vertex(l1, vertex_type="activity")
            v2 = new_graph.get_vertex(l2, vertex_type="activity")
            new_graph.add_edge(from_vertex=v1, to_vertex=v2, directed=True)
        return new_graph

    def as_dataframe(self) -> pd.DataFrame:
        graphs = [
            {"id": g.graph_id, "graph": g.unlink(), "vertices": g.vertices, "edges": g.edges, **g.metadata}
            for _, g in self.graphs.items()
        ]
        return pd.DataFrame.from_records(graphs, index="id")

    def load_subdue_results_from_file(self, file, graph_metadata: dict[str, Any] | None = None):
        """
        Loads the graph collection from a subdue results file.
        The result file is a human-readable report.
        """
        with open(file, "r") as f:
            lines = f.readlines()

        created_graphs = []
        current_graph: Graph | None = None

        for i, line in enumerate(lines):
            stripped = line.strip()
            if not stripped:
                continue

            if stripped == "S":
                current_graph = self.new_graph(metadata=graph_metadata)
                created_graphs.append(current_graph)
                continue

            if current_graph is not None:
                values = stripped.split()
                if not values:
                    continue

                if values[0] == "v":
                    label = " ".join(values[2:]).strip('"').strip()
                    current_graph.get_vertex(
                        label,
                        vertex_type=self.get_label_type_by_label(label),
                        vertex_id=int(values[1]) - 1,
                        force_create=True,
                    )
                elif values[0] in ("e", "d", "u"):
                    current_graph.add_edge(
                        from_vertex=current_graph.vertices[int(values[1]) - 1],
                        to_vertex=current_graph.vertices[int(values[2]) - 1],
                        directed=values[0] in ("e", "d"),
                    )
        return created_graphs

    def load_graphs_from_str_rep_file(self, file, graph_metadata: dict[str, Any] | None = None, variant="gspan"):
        """
        Loads the graph collection from a string representation file.
        Important: The label_mapping must be initialized before starting the import.

        e.g.

        - t # graph_id
        - v vertex_id vertex_attributes_id
        - v vertex_id vertex_attributes_id
        - ...
        - e vertex_source_id vertex_sink_id edge_type
        - e vertex_source_id vertex_sink_id edge_type
        - ...
        - t # graph_id
        - ...

        :param file: The file of the string representation
        """
        with open(file, "r") as f:
            lines = f.readlines()
            self.load_graphs_from_str_rep(lines, graph_metadata=graph_metadata, variant=variant)

    def load_graphs_from_str_rep(
        self, graph_str_rep: str | list[str], graph_metadata: dict[str, Any] | None = None, variant="gspan"
    ):
        """
        Loads the graph collection from a string representation.
        Important: The label_mapping must be initialized before starting the import!
        The labels are not stored in the string representation!

        e.g.

        - t # graph_id
        - v vertex_id vertex_attributes_id
        - v vertex_id vertex_attributes_id
        - ...
        - e vertex_source_id vertex_sink_id edge_type
        - e vertex_source_id vertex_sink_id edge_type
        - ...
        - t # graph_id

        :param graph_str_rep: The string representation
        :param graph_metadata: Additional graph metadata
        :return: the created graph
        """
        if isinstance(graph_str_rep, str):
            lines = graph_str_rep.split("\n")
        else:
            lines = graph_str_rep
        created_graphs = []
        current_graph: Graph | None = None
        for line in lines:
            values = line.split(" ")
            if values[0] == "t":  # gspan / cpd
                current_graph = self.new_graph(metadata=graph_metadata)
                created_graphs.append(current_graph)
                if len(values) > 4 and values[3] == "*":
                    current_graph.metadata[GRAPH_METADATA_SUPPORT] = int(values[4])
                if len(values) > 6 and values[5] == "/":
                    current_graph.metadata["support_relaxed"] = int(values[6])

            if values[0] == "v" and current_graph is not None:
                label = self.get_label(int(values[2]))
                vertex_type = self.get_label(int(values[3])) if len(values) > 3 else self.get_label_type_by_label(label)
                current_graph.get_vertex(
                    label,
                    vertex_id=int(values[1]),
                    vertex_type=vertex_type,
                    force_create=True,
                )
            if (values[0] == "e" or values[0] == "d") and current_graph is not None:  # e (gspan, cpd), d (subdue)
                current_graph.add_edge(
                    from_vertex=current_graph.vertices[int(values[1])],
                    to_vertex=current_graph.vertices[int(values[2])],
                    # type=int(values[3]),
                    directed=True,
                )

    def load_graph_from_heuristic_net(
        self, heu_net: HeuristicsNet, ignore_type=True, graph_metadata: dict[str, Any] | None = None
    ) -> Graph:
        """
        Loads a graph from a heuristic net.

        :param heu_net: The heuristic net
        :param ignore_type: Ignores the edge type (sets to 1)
        :param graph_metadata: Additional graph metadata
        :return: the created graph
        """
        graph = self.new_graph(metadata=graph_metadata)
        # Create the vertices
        for n_label, node in heu_net.nodes.items():
            graph.get_vertex(n_label, vertex_type="activity")
        # Create the edges
        for n_label, node in heu_net.nodes.items():
            g_node = graph.get_vertex(n_label)
            for other_node in node.output_connections:
                o_g_node = graph.get_vertex(other_node.node_name)
                g_node.add_edge(o_g_node, directed=True)
        return graph

    def __iter__(self) -> Iterator[Graph]:
        return iter(self.graphs.values())

    def __len__(self):
        return len(self.graphs)

    def __str__(self):
        return (
            f"GraphCollection with {len(self.graphs)} graphs, "
            f"{len(self.label_list)} labels, {len(self.clusters)} clusters"
        )

    def __repr__(self):
        return str(self)

collection_id property

Get the collection id of this collection

__init__(*, label_list=None, collection_id=None)

Creates a new GraphCollection.

:param label_list: The label_list as list or path to a pickel file containing the label_list, optional, can be created on the fly by building the graph_collection :param collection_id: An id for this collection, if None, a new unique id one will be created

Source code in src/collaboration_detection/datastructures/graph_collection.py
def __init__(self, *, label_list: list[tuple[str, str]] | str | None = None, collection_id: str | None = None):
    """
    Creates a new GraphCollection.

    :param label_list: The label_list as list or path to a pickel file containing the label_list,
    optional, can be created on the fly by building the graph_collection
    :param collection_id: An id for this collection, if None, a new unique id one will be created
    """

    self.label_mapping: dict[str, int] = {}
    self.label_list: list[tuple[str, str]] = []
    self.graphs: dict[int, Graph] = {}
    self.init_label_mapping(label_list)
    self.clusters: dict[int, GraphSetCluster] = {}
    self._collection_id: str = collection_id if collection_id is not None else str(uuid.uuid4())

as_edge_rep()

Creates an edge representation of the graphs. So the output of this function is a list of graphs. Each graph is represented as a dict. Each Edge is represented as a tuple of two strings, which are the both connected vertices. The value represents the type of the edge.

[{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

:return: a list of dicts (graphs as edge representation)

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_edge_rep(self) -> list[dict]:
    """
    Creates an edge representation of the graphs.
    So the output of this function is a list of graphs.
    Each graph is represented as a dict.
    Each Edge is represented as a tuple of two strings, which are the both connected vertices.
    The value represents the type of the edge.

    [{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

    :return: a list of dicts (graphs as edge representation)
    """
    return [g.as_edge_rep for g in self]

as_networkx_digraphs()

Creates a list of networkx.DiGraph objects.

:return: A list of networkx.DiGraph objects of the graphs

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_networkx_digraphs(self) -> list[nx.DiGraph]:
    """
    Creates a list of networkx.DiGraph objects.

    :return: A list of networkx.DiGraph objects of the graphs
    """
    return [g.as_networkx_digraph for g in self]

as_str_rep(variant='gspan')

Creates the string representation of the graph collection. Important: The labels are printed as integer values (the id of the label) The variant can be 'gspan' or 'subdue'.

  • t # graph_id
  • v vertex_id vertex_attributes_id
  • v vertex_id vertex_attributes_id
  • ...
  • e vertex_source_id vertex_sink_id edge_type
  • e vertex_source_id vertex_sink_id edge_type
  • ...
  • t # graph_id
  • ...

:return: The string representation of the graphs

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_str_rep(self, variant="gspan") -> str:
    """
    Creates the string representation of the graph collection.
    Important: The labels are printed as integer values (the id of the label)
    The variant can be 'gspan' or 'subdue'.

    - t # graph_id
    - v vertex_id vertex_attributes_id
    - v vertex_id vertex_attributes_id
    - ...
    - e vertex_source_id vertex_sink_id edge_type
    - e vertex_source_id vertex_sink_id edge_type
    - ...
    - t # graph_id
    - ...

    :return: The string representation of the graphs
    """
    return "\n".join(self.as_str_rep_lines(variant))

as_str_rep_lines(variant='gspan')

Creates the string representation of the graph collection. Important: The labels are printed as integer values (the id of the label) The variant can be 'gspan' or 'subdue'.

:return: a list of strings (the string representation of the graphs)

Source code in src/collaboration_detection/datastructures/graph_collection.py
def as_str_rep_lines(self, variant="gspan") -> list[str]:
    """
    Creates the string representation of the graph collection.
    Important: The labels are printed as integer values (the id of the label)
    The variant can be 'gspan' or 'subdue'.

    :return: a list of strings (the string representation of the graphs)
    """
    return [g.as_str_rep(variant) for g in self]

clean(label_list=None)

Clean the graph collection. Optional initialize the label mapping with the given label_list. The label_list should contain strings of the labels.

:param label_list: The list of labels. Or path to the pickle file.

Source code in src/collaboration_detection/datastructures/graph_collection.py
def clean(self, label_list: list[tuple[str, str]] | str | None = None):
    """
    Clean the graph collection. Optional initialize the label mapping with the given label_list.
    The label_list should contain strings of the labels.

    :param label_list: The list of labels. Or path to the pickle file.
    """
    self.graphs = {}
    self.init_label_mapping(label_list)
    self.clusters = {}

export(path)

Saves the graph collection as pickle file.

:file: the path of the file

Source code in src/collaboration_detection/datastructures/graph_collection.py
def export(self, path: str):
    """
    Saves the graph collection as pickle file.

    :file: the path of the file
    """
    with open(path, "wb") as f:
        pickle.dump(self, f, pickle.HIGHEST_PROTOCOL)

filter(vertex_count_min=None, vertex_count_max=None, edge_count_min=None, edge_count_max=None, vertex_label_in=None, vertex_label_is=None, cluster_id=None, expected_metadata=None, filter_func=None)

Creates an iterator for this graph collection. It iterates over all graphs and applies the given filter.

:param vertex_count_min: The graph should contain at least x vertices :param vertex_count_max: The graph should contain at maximum x vertices :param edge_count_min: The graph should contain at least x edges :param edge_count_max: The graph should contain at maximum x edges :param vertex_label_in: The graph should contain a vertex with a label matches a part of this value :param vertex_label_is: The graph should contain a vertex with a label matches this value :param cluster_id: The graph should be part of the given cluster_id :param expected_metadata: The graph should have the values of the specified expected_metadata :param filter_func: A filter function that gets a graph and should return true or false :return: Iterator over the graphs

Source code in src/collaboration_detection/datastructures/graph_collection.py
def filter(
    self,
    vertex_count_min: int | None = None,
    vertex_count_max: int | None = None,
    edge_count_min: int | None = None,
    edge_count_max: int | None = None,
    vertex_label_in: str | None = None,
    vertex_label_is: str | None = None,
    cluster_id: int | None = None,
    expected_metadata: dict[str, Any] | None = None,
    filter_func: Callable[[Graph], bool] | None = None,
) -> Iterator[Graph]:
    """
    Creates an iterator for this graph collection.
    It iterates over all graphs and applies the given filter.

    :param vertex_count_min: The graph should contain at least x vertices
    :param vertex_count_max: The graph should contain at maximum x vertices
    :param edge_count_min: The graph should contain at least x edges
    :param edge_count_max: The graph should contain at maximum x edges
    :param vertex_label_in: The graph should contain a vertex with a label matches a part of this value
    :param vertex_label_is: The graph should contain a vertex with a label matches this value
    :param cluster_id: The graph should be part of the given cluster_id
    :param expected_metadata: The graph should have the values of the specified expected_metadata
    :param filter_func: A filter function that gets a graph and should return true or false
    :return: Iterator over the graphs
    """
    graphs: GraphCollection | set[Graph] = self
    if cluster_id is not None:
        cluster = self.clusters.get(cluster_id)
        graphs = cluster.graphs if cluster is not None else set()
    for g in graphs:
        if vertex_count_min is not None and len(g.vertices) < vertex_count_min:
            continue
        if vertex_count_max is not None and len(g.vertices) > vertex_count_max:
            continue
        if edge_count_min is not None and len(g.edges) < edge_count_min:
            continue
        if edge_count_max is not None and len(g.edges) > edge_count_max:
            continue
        if vertex_label_in is not None and not any(vertex_label_in in v.label for v_id, v in g.vertices.items()):
            continue
        if vertex_label_is is not None and not any(vertex_label_is == v.label for v_id, v in g.vertices.items()):
            continue
        if cluster_id is not None and g.cluster_id == cluster_id:
            continue
        if filter_func is not None and not filter_func(g):
            continue
        if expected_metadata is not None and not all(
            g.metadata.get(key, None) == exp_value for key, exp_value in expected_metadata.items()
        ):
            continue

        yield g

get_label(label_id)

Get the label for the given label id

:param label_id: the id of the label :return: the label as string or None if the id is not present

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_label(self, label_id: int) -> str | None:
    """
    Get the label for the given label id

    :param label_id: the id of the label
    :return: the label as string or None if the id is not present
    """
    if len(self.label_list) <= label_id:
        return None
    return self.label_list[label_id][0]

get_label_type(label_id)

Get the label type for the given label id

:param label_id: the id of the label :return: the label type as string or None if the id or none if label type is None for the label id is not present

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_label_type(self, label_id: int) -> str | None:
    """
    Get the label type for the given label id

    :param label_id: the id of the label
    :return: the label type as string or None if the id or none if label type is None for the label id is not present
    """
    if len(self.label_list) <= label_id:
        return None
    return self.label_list[label_id][1]

get_label_type_by_label(label)

Get the label type for the given label

:param label: the label as string :return: the label type as string or None if the id or none if label type is None for the label id is not present

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_label_type_by_label(self, label: str) -> str | None:
    """
    Get the label type for the given label

    :param label: the label as string
    :return: the label type as string or None if the id or none if label type is None for the label id is not present
    """
    label_id = self.get_set_label_id(label)
    return self.label_list[label_id][1]

get_set_label_id(label, label_type=None)

Get the id of the given label. If the label is not present, it is created for this graph_collection.

:param label: the label as string :return: the id of the label

Source code in src/collaboration_detection/datastructures/graph_collection.py
def get_set_label_id(self, label: str, label_type: str | None = None) -> int:
    """
    Get the id of the given label. If the label is not present, it is created for this graph_collection.

    :param label: the label as string
    :return: the id of the label
    """
    label = label.strip()
    if label_type is None:
        label_type = ""
    if label_type != "" and label != label_type:
        self.get_set_label_id(label_type, label_type)
    label_id = self.label_mapping.get(label, None)
    if label_id is not None:
        return label_id
    else:
        new_id = len(self.label_list)
        self.label_list.append((label, label_type))
        self.label_mapping[label] = new_id
        return new_id

init_label_mapping(label_list=None, format='pickle')

Init the label mapping with the given list of label tuples. The index of the label is the id of the label mapping. The param label_list can be a path to a pickle file which was created with 'save_label_mapping'.

:param label_list: The list of labels. Or path to the pickle file or text file. :param format: The format of the file (pickle or text), if label_list is the path to the file.

Source code in src/collaboration_detection/datastructures/graph_collection.py
def init_label_mapping(self, label_list: list[tuple[str, str]] | str | None = None, format="pickle"):
    """
    Init the label mapping with the given list of label tuples.
    The index of the label is the id of the label mapping.
    The param label_list can be a path to a pickle file which was created with 'save_label_mapping'.

    :param label_list: The list of labels. Or path to the pickle file or text file.
    :param format: The format of the file (pickle or text), if label_list is the path to the file.
    """
    # dict {('a'):1,('b'):0}
    self.label_mapping = {}
    if label_list is None:
        # list [('b'),('a')]
        self.label_list = []
    elif isinstance(label_list, str):
        with open(label_list, "rb" if format == "pickle" else "r") as file:
            if format == "pickle":
                self.label_list = pickle.load(file)
            elif format == "text":
                self.label_list = [tuple(line.strip().split(";;;")) for line in file]
    else:
        self.label_list = deepcopy(label_list)
    for i, (val, _) in enumerate(self.label_list):
        self.label_mapping[val] = i

load(path) staticmethod

Loads the graph collection from a pickle file

:file: the path of the file

Source code in src/collaboration_detection/datastructures/graph_collection.py
@staticmethod
def load(path: str) -> "GraphCollection":
    """
    Loads the graph collection from a pickle file

    :file: the path of the file
    """
    with open(path, "rb") as f:
        model: GraphCollection = pickle.load(f)
        return model

load_graph_from_heuristic_net(heu_net, ignore_type=True, graph_metadata=None)

Loads a graph from a heuristic net.

:param heu_net: The heuristic net :param ignore_type: Ignores the edge type (sets to 1) :param graph_metadata: Additional graph metadata :return: the created graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def load_graph_from_heuristic_net(
    self, heu_net: HeuristicsNet, ignore_type=True, graph_metadata: dict[str, Any] | None = None
) -> Graph:
    """
    Loads a graph from a heuristic net.

    :param heu_net: The heuristic net
    :param ignore_type: Ignores the edge type (sets to 1)
    :param graph_metadata: Additional graph metadata
    :return: the created graph
    """
    graph = self.new_graph(metadata=graph_metadata)
    # Create the vertices
    for n_label, node in heu_net.nodes.items():
        graph.get_vertex(n_label, vertex_type="activity")
    # Create the edges
    for n_label, node in heu_net.nodes.items():
        g_node = graph.get_vertex(n_label)
        for other_node in node.output_connections:
            o_g_node = graph.get_vertex(other_node.node_name)
            g_node.add_edge(o_g_node, directed=True)
    return graph

load_graphs_from_edge_rep(graph_edge_rep, ignore_type=True)

Loads the graph collection from an edge representation.

e.g. [{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

:param graph_edge_rep: The edge representation object :param ignore_type: Ignores the edge type (sets to 1) :return: the created graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def load_graphs_from_edge_rep(
    self,
    graph_edge_rep: dict[tuple[str, str], int],
    ignore_type=True,
) -> Graph:
    """
    Loads the graph collection from an edge representation.

    e.g.  [{('A','B'):2,('B','C'):1},{('A','C'):1,('C','B'): 1},...]

    :param graph_edge_rep: The edge representation object
    :param ignore_type: Ignores the edge type (sets to 1)
    :return: the created graph
    """
    new_graph = self.new_graph()
    for l1, l2 in graph_edge_rep:
        v1 = new_graph.get_vertex(l1, vertex_type="activity")
        v2 = new_graph.get_vertex(l2, vertex_type="activity")
        new_graph.add_edge(from_vertex=v1, to_vertex=v2, directed=True)
    return new_graph

load_graphs_from_str_rep(graph_str_rep, graph_metadata=None, variant='gspan')

Loads the graph collection from a string representation. Important: The label_mapping must be initialized before starting the import! The labels are not stored in the string representation!

e.g.

  • t # graph_id
  • v vertex_id vertex_attributes_id
  • v vertex_id vertex_attributes_id
  • ...
  • e vertex_source_id vertex_sink_id edge_type
  • e vertex_source_id vertex_sink_id edge_type
  • ...
  • t # graph_id

:param graph_str_rep: The string representation :param graph_metadata: Additional graph metadata :return: the created graph

Source code in src/collaboration_detection/datastructures/graph_collection.py
def load_graphs_from_str_rep(
    self, graph_str_rep: str | list[str], graph_metadata: dict[str, Any] | None = None, variant="gspan"
):
    """
    Loads the graph collection from a string representation.
    Important: The label_mapping must be initialized before starting the import!
    The labels are not stored in the string representation!

    e.g.

    - t # graph_id
    - v vertex_id vertex_attributes_id
    - v vertex_id vertex_attributes_id
    - ...
    - e vertex_source_id vertex_sink_id edge_type
    - e vertex_source_id vertex_sink_id edge_type
    - ...
    - t # graph_id

    :param graph_str_rep: The string representation
    :param graph_metadata: Additional graph metadata
    :return: the created graph
    """
    if isinstance(graph_str_rep, str):
        lines = graph_str_rep.split("\n")
    else:
        lines = graph_str_rep
    created_graphs = []
    current_graph: Graph | None = None
    for line in lines:
        values = line.split(" ")
        if values[0] == "t":  # gspan / cpd
            current_graph = self.new_graph(metadata=graph_metadata)
            created_graphs.append(current_graph)
            if len(values) > 4 and values[3] == "*":
                current_graph.metadata[GRAPH_METADATA_SUPPORT] = int(values[4])
            if len(values) > 6 and values[5] == "/":
                current_graph.metadata["support_relaxed"] = int(values[6])

        if values[0] == "v" and current_graph is not None:
            label = self.get_label(int(values[2]))
            vertex_type = self.get_label(int(values[3])) if len(values) > 3 else self.get_label_type_by_label(label)
            current_graph.get_vertex(
                label,
                vertex_id=int(values[1]),
                vertex_type=vertex_type,
                force_create=True,
            )
        if (values[0] == "e" or values[0] == "d") and current_graph is not None:  # e (gspan, cpd), d (subdue)
            current_graph.add_edge(
                from_vertex=current_graph.vertices[int(values[1])],
                to_vertex=current_graph.vertices[int(values[2])],
                # type=int(values[3]),
                directed=True,
            )

load_graphs_from_str_rep_file(file, graph_metadata=None, variant='gspan')

Loads the graph collection from a string representation file. Important: The label_mapping must be initialized before starting the import.

e.g.

  • t # graph_id
  • v vertex_id vertex_attributes_id
  • v vertex_id vertex_attributes_id
  • ...
  • e vertex_source_id vertex_sink_id edge_type
  • e vertex_source_id vertex_sink_id edge_type
  • ...
  • t # graph_id
  • ...

:param file: The file of the string representation

Source code in src/collaboration_detection/datastructures/graph_collection.py
def load_graphs_from_str_rep_file(self, file, graph_metadata: dict[str, Any] | None = None, variant="gspan"):
    """
    Loads the graph collection from a string representation file.
    Important: The label_mapping must be initialized before starting the import.

    e.g.

    - t # graph_id
    - v vertex_id vertex_attributes_id
    - v vertex_id vertex_attributes_id
    - ...
    - e vertex_source_id vertex_sink_id edge_type
    - e vertex_source_id vertex_sink_id edge_type
    - ...
    - t # graph_id
    - ...

    :param file: The file of the string representation
    """
    with open(file, "r") as f:
        lines = f.readlines()
        self.load_graphs_from_str_rep(lines, graph_metadata=graph_metadata, variant=variant)

load_subdue_results_from_file(file, graph_metadata=None)

Loads the graph collection from a subdue results file. The result file is a human-readable report.

Source code in src/collaboration_detection/datastructures/graph_collection.py
def load_subdue_results_from_file(self, file, graph_metadata: dict[str, Any] | None = None):
    """
    Loads the graph collection from a subdue results file.
    The result file is a human-readable report.
    """
    with open(file, "r") as f:
        lines = f.readlines()

    created_graphs = []
    current_graph: Graph | None = None

    for i, line in enumerate(lines):
        stripped = line.strip()
        if not stripped:
            continue

        if stripped == "S":
            current_graph = self.new_graph(metadata=graph_metadata)
            created_graphs.append(current_graph)
            continue

        if current_graph is not None:
            values = stripped.split()
            if not values:
                continue

            if values[0] == "v":
                label = " ".join(values[2:]).strip('"').strip()
                current_graph.get_vertex(
                    label,
                    vertex_type=self.get_label_type_by_label(label),
                    vertex_id=int(values[1]) - 1,
                    force_create=True,
                )
            elif values[0] in ("e", "d", "u"):
                current_graph.add_edge(
                    from_vertex=current_graph.vertices[int(values[1]) - 1],
                    to_vertex=current_graph.vertices[int(values[2]) - 1],
                    directed=values[0] in ("e", "d"),
                )
    return created_graphs

new_graph(*, graph_id=None, cluster_id=None, metadata=None, **kwargs)

Creates a new graph in this graph collection. The graph gets a new unique id.

:param graph_id: An id for the graph :param cluster_id: An id for the cluster the graph belongs to :param metadata: Optional additional metadata as dict. :return: a new Graph object

Source code in src/collaboration_detection/datastructures/graph_collection.py
def new_graph(
    self,
    *,
    graph_id: int | None = None,
    cluster_id: int | None = None,
    metadata: dict | None = None,
    **kwargs,
) -> Graph:
    """
    Creates a new graph in this graph collection. The graph gets a new unique id.

    :param graph_id: An id for the graph
    :param cluster_id: An id for the cluster the graph belongs to
    :param metadata: Optional additional metadata as dict.
    :return: a new Graph object
    """
    graph_id = graph_id if graph_id is not None else len(self.graphs)
    g = Graph(self, graph_id, cluster_id=cluster_id, metadata=metadata, **kwargs)
    self.graphs[graph_id] = g
    return g

save_label_mapping(file_path, format='pickle')

Save the label mapping in a pickle file

:param file_path: the file_path of the output :param format: "pickle" if pickle should be used, "text" plain if a text file should be used

Source code in src/collaboration_detection/datastructures/graph_collection.py
def save_label_mapping(self, file_path, format="pickle"):
    """
    Save the label mapping in a pickle file

    :param file_path: the file_path of the output
    :param format: "pickle" if pickle should be used, "text" plain if a text file should be used
    """
    with open(file_path, "wb" if format == "pickle" else "w") as file:
        if format == "pickle":
            pickle.dump(self.label_list, file=file)
        elif format == "text":
            file.write(os.linesep.join(f"{label};;;{l_type}" for (label, l_type) in self.label_list))

save_str_rep(file, variant='gspan')

Saves the graph collection as string representation as file. The variant can be 'gspan' or 'subdue'.

:param file: The path or the file object

Source code in src/collaboration_detection/datastructures/graph_collection.py
def save_str_rep(self, file, variant="gspan"):
    """
    Saves the graph collection as string representation as file.
    The variant can be 'gspan' or 'subdue'.

    :param file: The path or the file object
    """
    if isinstance(file, str):
        with open(file, "w") as f:
            f.write(self.as_str_rep(variant))
            f.flush()
    else:
        file.write(self.as_str_rep(variant))
        file.flush()

tar_images(path, format='svg')

Exports the graphs of a GraphCollection to a tar file (images of the graphs).

:param path: The base path where the tar file will be saved. :param format: The format of the images, Default: svg

Source code in src/collaboration_detection/datastructures/graph_collection.py
def tar_images(self, path: str, format: str = "svg"):
    """
    Exports the graphs of a GraphCollection to a tar file (images of the graphs).

    :param path: The base path where the tar file will be saved.
    :param format: The format of the images, Default: svg
    """
    tar_images_of_g_c(self, path, format)

zip_images(path, format='svg')

Exports the graphs of a GraphCollection to a zip file (images of the graphs).

:param path: The base path where the zip file will be saved. :param format: The format of the images, Default: svg

Source code in src/collaboration_detection/datastructures/graph_collection.py
def zip_images(self, path: str, format: str = "svg"):
    """
    Exports the graphs of a GraphCollection to a zip file (images of the graphs).

    :param path: The base path where the zip file will be saved.
    :param format: The format of the images, Default: svg
    """
    zip_images_of_g_c(self, path, format)

Neo4j

Bases: BaseRepository

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
class GraphCollectionRepository(BaseRepository):
    def __init__(self, uri: str, user: str, password: str, **kwargs):
        super().__init__(uri, user, password, **kwargs)
        self.create_indices()
        object_id_generator = CypherObjectIdGenerator("o")
        self.object_id_generator = object_id_generator
        self.collection_mapper = CollectionCypherMapper(object_id_generator)
        self.cluster_mapper = ClusterCypherMapper(object_id_generator)
        self.vertex_label_mapper = VertexLabelCypherMapper(object_id_generator)
        self.graph_mapper = GraphCypherMapper(object_id_generator)
        self.part_of_cluster_relation_mapper = PartOfClusterRelationCypherMapper(object_id_generator)
        self.graph_is_part_of_collection_relation_mapper = GraphPartOfCollectionRelationCypherMapper(
            object_id_generator
        )
        self.cluster_is_part_of_collection_relation_mapper = ClusterPartOfCollectionRelationCypherMapper(
            object_id_generator
        )
        self.is_representative_relation_mapper = IsRepresentativeRelationCypherMapper(object_id_generator)
        self.has_vertex_with_label_relation_mapper = HasVertexWithLabelRelationCypherMapper(object_id_generator)
        self.has_edge_relation_mapper = HasEdgeRelationCypherMapper(object_id_generator)

    def upload_graph_collection(self, graph_collection: GraphCollection):
        """Upload the graph collection
        Attention: The existing graph collection will be deleted beforehand.

        :param graph_collection: The Graph Collection
        """
        with self.driver.session() as session:
            session.execute_write(self._upload_graph_collection, graph_collection=graph_collection)

    def _upload_graph_collection(self, tx: ManagedTransaction, graph_collection: GraphCollection):
        self.object_id_generator.reset()
        # 0. Create the graph collection node
        self._delete_graph_collection(tx, graph_collection.collection_id)
        collection_a = {"collection_id": graph_collection.collection_id, "created": datetime.datetime.now(datetime.UTC)}
        c_q, c_id, collection_a = self.collection_mapper.get_cypher_str(parameters=collection_a)
        query = f"CREATE {c_q}"
        self._run_tx(tx, query, parameters=collection_a)

        # Graph Merge query
        graph_merge_a: dict[str, Any] = {"graph_id": 0, **{key: None for g in graph_collection for key in g.metadata}}

        graph_q, g_id, graph_merge_a = self.graph_mapper.get_cypher_str(parameters=graph_merge_a)
        # 1.2. Create the relationship to the collection node
        collection_r_q, _, _ = self.graph_is_part_of_collection_relation_mapper.get_cypher_str(add_cypher_label=False)
        graph_merge_q = f"MATCH {c_q}"
        graph_merge_q += f"CREATE {graph_q}-{collection_r_q}->({c_id})"

        # Graph MATCH query
        graph_q, g_id, _graph_match_a = self.graph_mapper.get_cypher_str(parameters={"graph_id": 0})
        graph_match_q = f"MATCH {c_q}<-{collection_r_q}-{graph_q} "

        # 1. Add the graph
        for graph in graph_collection:
            # 1.1. Add graph node
            graph_a = collection_a | graph_merge_a | {"graph_id": graph.graph_id, **graph.metadata}
            self._run_tx(tx, graph_merge_q, parameters=graph_a)

        # 2. Add the vertex labels
        # Vertex Label MERGE Query
        vertex_label_q, vl_id, _ = self.vertex_label_mapper.get_cypher_str(parameters={"label": ""})
        vertex_label_merge_q = f"MERGE {vertex_label_q} "
        for label, _ in graph_collection.label_list:
            self._run_tx(tx, vertex_label_merge_q, parameters={"label": label})

        vertex_label_match_q = f"MATCH {vertex_label_q} "

        # 2.1 Add the vertices (as relations)
        # Vertex MERGE Query
        vertex_merge_a: dict[str, Any] = {
            "vertex_id": 0,
            **{key: None for g in graph_collection for v_id, v in g.vertices.items() for key in v.metadata},
        }
        vertex_r_q, _v_id, vertex_merge_a = self.has_vertex_with_label_relation_mapper.get_cypher_str(
            parameters=vertex_merge_a
        )
        vertex_merge_q = graph_match_q + vertex_label_match_q
        vertex_merge_q += f"CREATE ({g_id})-{vertex_r_q}->({vl_id}) "

        for graph in graph_collection:
            graph_a = vertex_merge_a | collection_a | {"graph_id": graph.graph_id}
            for vertex in graph.vertices.values():
                vertex_a = graph_a | {"vertex_id": vertex.vertex_id, "label": vertex.label} | vertex.metadata
                self._run_tx(tx, vertex_merge_q, parameters=vertex_a)

        # Edge MERGE Query
        edge_a: dict[str, Any] = {
            "from_vertex_id": "",
            "to_vertex_id": "",
            **{key: None for g in graph_collection for e in g.edges for key in e.metadata},
        }
        edge_q, _e_id, edge_a = self.has_edge_relation_mapper.get_cypher_str(parameters=edge_a)
        edge_merge_query = graph_match_q + vertex_label_match_q
        edge_merge_query += f"CREATE ({g_id})-{edge_q}->({vl_id})"

        for graph in graph_collection:
            graph_a = {"graph_id": graph.graph_id}
            for edge in graph.edges:
                edge_params = (
                    collection_a
                    | graph_a
                    | edge_a
                    | {
                        "label": edge.from_vertex.label,
                        "from_vertex_id": edge.from_vertex.vertex_id,
                        "to_vertex_id": edge.to_vertex.vertex_id,
                        **{key: value for key, value in edge.metadata.items()},
                    }
                )
                self._run_tx(tx, edge_merge_query, parameters=edge_params)
        # Add cluster
        self._set_clusters(tx, graph_collection)

    def delete_graph_collection(self, graph_collection: str | GraphCollection):
        """Delete the graph collection in the repository.
        Delete all nodes of this graph collection.

        :param graph_collection: The Graph Collection
        """
        with self.driver.session() as session:
            session.execute_write(
                self._delete_graph_collection,
                collection_id=graph_collection.collection_id
                if isinstance(graph_collection, GraphCollection)
                else graph_collection,
            )

    def _delete_graph_collection(self, tx: ManagedTransaction, collection_id: str):
        self.object_id_generator.reset()
        collection_attributes = {
            "collection_id": collection_id,
        }
        c_q, c_id, collection_attributes = self.collection_mapper.get_cypher_str(parameters=collection_attributes)
        query = f"MATCH {c_q}--(n) DETACH DELETE n,{c_id}"
        tx.run(query, parameters=collection_attributes)

    def set_graph_metadata(self, graph_collection: GraphCollection, graph_id: int | None):
        """Saves the metadata for the given graph (or all graphs)

        :param graph_collection: The Graph Collection
        :param graph_id: Optional: The Graph Id, if the metadata of a single graph should be saved
        """
        with self.driver.session() as session:
            return session.execute_write(
                self._set_graph_metadata,
                graph_collection=graph_collection,
                graph_id=graph_id,
            )

    def _set_graph_metadata(self, tx: ManagedTransaction, graph_collection: GraphCollection, graph_id: int | None):
        if graph_id is None:
            for g_id in graph_collection.graphs:
                self._set_graph_metadata(tx, graph_collection, graph_id=g_id)
            return
        graph = graph_collection.graphs[graph_id]
        query = """
                MATCH (collection:Collection {collection_id:$collection_id})
                MATCH (collection)<-[:GRAPH_IS_PART_OF_COLLECTION]-(graph:Graph {graph_id:$graph_id})
                SET graph = {graph_id:$graph_id}
                """
        if len(graph.metadata) > 0:
            query += f"SET graph += {parameters_to_cypher_str(graph.metadata)}"
        self._run_tx(
            tx,
            query,
            parameters={"collection_id": graph_collection.collection_id, "graph_id": graph_id} | graph.metadata,
        )

    def set_cluster_attributes(self, graph_collection: GraphCollection, cluster_id: int | None):
        """Saves the attributes for the given cluster

        :param graph_collection: The Graph Collection
        :param cluster_id: Optional: The Cluster Id, if the attributes of a single cluster should be saved
        """
        with self.driver.session() as session:
            return session.execute_write(
                self._set_cluster_attributes,
                graph_collection=graph_collection,
                cluster_id=cluster_id,
            )

    def _set_cluster_attributes(
        self, tx: ManagedTransaction, graph_collection: GraphCollection, cluster_id: int | None
    ):
        if cluster_id is None:
            for c_id in graph_collection.clusters:
                self._set_cluster_attributes(tx, graph_collection, cluster_id=c_id)
            return
        cluster = graph_collection.clusters[cluster_id]
        query = """
                MATCH (collection:Collection {collection_id:$collection_id})
                MATCH (collection)<-[:CLUSTER_IS_PART_OF_COLLECTION]-(cluster:Cluster {cluster_id:$cluster_id})
                SET cluster = {cluster_id:$cluster_id}
                """
        if len(cluster.attributes) > 0:
            query += f"SET cluster += {parameters_to_cypher_str(cluster.attributes)}"
        self._run_tx(
            tx,
            query,
            parameters={"collection_id": graph_collection.collection_id, "cluster_id": cluster_id} | cluster.attributes,
        )

    def get_collections(self) -> list[CollectionInfo]:
        """Get all collections (overview, not data) of the repository"""
        with self.driver.session() as session:
            return session.execute_read(self._get_collections)

    def _get_collections(self, tx: ManagedTransaction) -> list[CollectionInfo]:
        c_q, c_id, _ = self.collection_mapper.get_cypher_str()
        query = f"MATCH {c_q} "
        gr_q, _, _ = self.graph_is_part_of_collection_relation_mapper.get_cypher_str(add_cypher_label=False)
        g_q, g_id, _ = self.graph_mapper.get_cypher_str()
        query += f"OPTIONAL MATCH ({c_id})<-{gr_q}-{g_q} "
        query += f"WITH {c_id}, count({g_id}) AS graph_count "
        clr_q, _, _ = self.cluster_is_part_of_collection_relation_mapper.get_cypher_str(add_cypher_label=False)
        cl_q, cl_id, _ = self.cluster_mapper.get_cypher_str()
        query += f"OPTIONAL MATCH ({c_id})<-{clr_q}-{cl_q} "
        query += (
            f"RETURN {c_id}.collection_id as collection_id, {c_id}.created as created, "
            f"graph_count, count({cl_id}) as cluster_count "
        )
        query += "ORDER BY created"
        result = self._run_tx(tx, query, {})
        result_list = []
        for row in result:
            result_list.append(
                CollectionInfo(
                    collection_id=row.get("collection_id"),
                    created=row.get("created"),
                    number_of_graphs=row.get("graph_count"),
                    number_of_clusters=row.get("cluster_count"),
                )
            )
        return result_list

    def download_graph_collection(
        self, collection_id: str, graph_id: int | None = None, cluster_id: int | None = None
    ) -> GraphCollection:
        """Download the Graph Collection with the given ID"""
        with self.driver.session() as session:
            return session.execute_read(
                self._download_graph_collection, collection_id=collection_id, graph_id=graph_id, cluster_id=cluster_id
            )

    def _download_graph_collection(
        self,
        tx: ManagedTransaction,
        collection_id: str,
        graph_id: int | None = None,
        cluster_id: int | None = None,
    ) -> GraphCollection:
        query = "MATCH (collection:Collection {collection_id:$collection_id})"
        if graph_id is not None:
            query += "MATCH (collection)<-[:GRAPH_IS_PART_OF_COLLECTION]-(graph:Graph {graph_id:$graph_id})"
        else:
            query += "MATCH (collection)<-[:GRAPH_IS_PART_OF_COLLECTION]-(graph:Graph)"
        if cluster_id is not None:
            query += "MATCH (graph)-[:IS_PART_OF_CLUSTER]->(cluster:Cluster{cluster_id:$cluster_id})"
        else:
            query += "OPTIONAL MATCH (graph)-[:IS_PART_OF_CLUSTER]->(cluster:Cluster)"

        query += """OPTIONAL MATCH (graph)-[rep:IS_REPRESENTATIVE_OF]->(cluster)
                    WITH collection,graph,cluster,rep
                    OPTIONAL MATCH (graph)-[v:HAS_VERTEX_WITH_LABEL]->(vl:VertexLabel)
                    WITH collection,graph,cluster,rep,collect(v) AS vertices, collect(vl) AS vertex_labels
                    OPTIONAL MATCH (graph)-[e:HAS_EDGE]->(vl:VertexLabel)
                    WITH collection,graph,cluster,rep,vertices,vertex_labels,collect(e) AS edges
                    RETURN collection,graph,cluster,rep,vertices,vertex_labels,edges
                    """

        result = self._run_tx(
            tx, query, parameters={"collection_id": collection_id, "graph_id": graph_id, "cluster_id": cluster_id}
        )
        g_c = GraphCollection(collection_id=collection_id)
        if result is None:
            return g_c
        for record in result:
            cl = record.get("cluster")
            graph = g_c.new_graph(
                graph_id=record.get("graph").get("graph_id"),
                cluster_id=cl.get("cluster_id") if cl else None,
                metadata={key: value for key, value in record.get("graph").items() if key != "graph_id"},
            )
            for v, v_l in zip(record.get("vertices"), record.get("vertex_labels")):
                graph.get_vertex(
                    v_l.get("label"),
                    vertex_id=v.get("vertex_id"),
                    metadata={key: value for key, value in v.items() if key != "vertex_id"},
                    force_create=True,
                )
            for e in record.get("edges"):
                v_from = graph.get_vertex_by_id(e.get("from_vertex_id"))
                v_to = graph.get_vertex_by_id(e.get("to_vertex_id"))
                if v_from is not None and v_to is not None:
                    graph.add_edge(
                        v_from,
                        v_to,
                        edge_metadata={
                            key: value for key, value in e.items() if key not in {"from_vertex_id", "to_vertex_id"}
                        },
                    )
            if cl:
                cl_id = cl.get("cluster_id")
                cluster = g_c.clusters.get(cl_id)
                if cluster is None:
                    cluster = GraphSetCluster(
                        representative=None,
                        cluster_id=cl_id,
                        attributes={key: value for key, value in cl.items() if key != "cluster_id"},
                    )
                    g_c.clusters[cl_id] = cluster
                cluster.graphs.add(graph)
                if record.get("rep") is not None:
                    cluster.representative = graph
        return g_c

    def get_number_of_graphs(self, collection_id: str, cluster_id: int | None = None) -> int:
        """Get the size of a graph collection, optional filtered by the cluster_id for the size of the cluster"""
        with self.driver.session() as session:
            return session.execute_read(self._get_number_of_graphs, collection_id=collection_id, cluster_id=cluster_id)

    def _get_number_of_graphs(
        self, tx: ManagedTransaction, collection_id: str, cluster_id: int | None = None
    ) -> int:
        c_q, c_id, collection_a = self.collection_mapper.get_cypher_str(parameters={"collection_id": collection_id})

        query = f"MATCH {c_q}"
        graph_q, g_id, _ = self.graph_mapper.get_cypher_str()
        params = collection_a
        if cluster_id is None:
            collection_r_q, _, _ = self.graph_is_part_of_collection_relation_mapper.get_cypher_str(
                add_cypher_label=False
            )
            query += f"MATCH {graph_q}-{collection_r_q}->({c_id})"
        else:
            cluster_r_q, _, _ = self.cluster_is_part_of_collection_relation_mapper.get_cypher_str(
                add_cypher_label=False
            )
            cluster_q, _, cluster_a = self.cluster_mapper.get_cypher_str(
                add_cypher_label=False, parameters={"cluster_id": cluster_id}
            )
            params = params | cluster_a
            part_cluster_q, _, _ = self.part_of_cluster_relation_mapper.get_cypher_str(add_cypher_label=False)
            query += f"MATCH ({c_id})<-{cluster_r_q}-{cluster_q}-{part_cluster_q}-{graph_q}"
        query += f"RETURN COUNT({g_id}) AS cnt"
        result = self._run_tx(tx, query, params)
        return int(result.single().get("cnt"))

    def get_number_of_clusters(self, collection_id: str) -> int:
        """Get the number of clusters of this graph collection"""
        with self.driver.session() as session:
            return session.execute_read(
                self._get_number_of_clusters,
                collection_id=collection_id,
            )

    def _get_number_of_clusters(self, tx: ManagedTransaction, collection_id: str) -> int:
        c_q, _c_id, collection_a = self.collection_mapper.get_cypher_str(parameters={"collection_id": collection_id})
        cluster_r_q, _, _ = self.cluster_is_part_of_collection_relation_mapper.get_cypher_str(add_cypher_label=False)
        cluster_q, cluster_id, _ = self.cluster_mapper.get_cypher_str()
        query = f"MATCH {c_q}<-{cluster_r_q}-{cluster_q}"
        query += f"RETURN COUNT({cluster_id}) AS cnt"
        result = self._run_tx(tx, query, collection_a)
        return int(result.single().get("cnt"))

    def set_clusters(self, graph_collection: GraphCollection, cluster_id: int | None = None):
        """Save all cluster information for the given graph_collection"""
        with self.driver.session() as session:
            return session.execute_write(self._set_clusters, graph_collection=graph_collection, cluster_id=cluster_id)

    def _set_clusters(
        self, tx: ManagedTransaction, graph_collection: GraphCollection, cluster_id: int | None = None
    ):
        # Delete existing clusters
        self._delete_graph_collection_clusters(tx, graph_collection.collection_id, cluster_id)

        self.object_id_generator.reset()

        c_q, c_id, collection_a = self.collection_mapper.get_cypher_str(
            parameters={"collection_id": graph_collection.collection_id}
        )
        # 1.2. Create the relationship to the collection node
        collection_r_q, _, _ = self.graph_is_part_of_collection_relation_mapper.get_cypher_str(add_cypher_label=False)

        # Graph MATCH query
        graph_q, g_id, _graph_match_a = self.graph_mapper.get_cypher_str(parameters={"graph_id": 0})

        # Add cluster
        cluster_q, cluster_q_id, cluster_merge_a = self.cluster_mapper.get_cypher_str(
            {
                "cluster_id": 0,
                **{key: None for c in graph_collection.clusters.values() for key in c.attributes},
            }
        )
        cluster_merge_q = f"MATCH {c_q} "
        cluster_collection_r_q, _, _ = self.cluster_is_part_of_collection_relation_mapper.get_cypher_str(
            add_cypher_label=False
        )
        cluster_merge_q += f"MERGE {cluster_q}-{cluster_collection_r_q}->({c_id})"

        cluster_q, cluster_q_id, _cluster_match_a = self.cluster_mapper.get_cypher_str(
            {
                "cluster_id": 0,
            }
        )
        cluster_graph_match_q = f"MATCH {cluster_q}-{cluster_collection_r_q}->{c_q}"
        cluster_graph_match_q += f"MATCH ({c_id})<-{collection_r_q}-{graph_q} "

        # representative MERGE
        cluster_representative_q = cluster_graph_match_q
        is_representative_r_q, _, _ = self.is_representative_relation_mapper.get_cypher_str(add_cypher_label=False)
        cluster_representative_q += f"CREATE ({g_id})-{is_representative_r_q}->({cluster_q_id})"

        # graph to
        cluster_relation_q = cluster_graph_match_q
        cluster_part_r_q, _, _ = self.part_of_cluster_relation_mapper.get_cypher_str(add_cypher_label=False)
        cluster_relation_q += f"CREATE ({g_id})-{cluster_part_r_q}->({cluster_q_id}) "

        # 4. Add cluster
        for _, cluster in (
            graph_collection.clusters.items()
            if cluster_id is None
            else [(cluster_id, graph_collection.clusters[cluster_id])]
        ):
            # 4.1. Create cluster node with relation to the collection
            cluster_a = collection_a | cluster_merge_a | {"cluster_id": cluster.cluster_id, **cluster.attributes}
            self._run_tx(tx, cluster_merge_q, parameters=cluster_a)
            cluster_a = {
                "cluster_id": cluster.cluster_id,
            }
            # 4.3. Create relationships from representative
            if cluster.representative:
                rep_a = (
                    collection_a
                    | cluster_a
                    | {
                        "graph_id": cluster.representative.graph_id,
                    }
                )
                self._run_tx(tx, cluster_representative_q, parameters=rep_a)
            # 4.3. Create relationships from graphs
            for g in cluster.graphs:
                rep_a = (
                    collection_a
                    | cluster_a
                    | {
                        "graph_id": g.graph_id,
                    }
                )
                self._run_tx(tx, cluster_relation_q, parameters=rep_a)

    def _delete_graph_collection_clusters(self, tx: ManagedTransaction, collection_id: str, cluster_id: int | None):
        c_q, c_id, collection_a = self.collection_mapper.get_cypher_str(parameters={"collection_id": collection_id})
        query = f"MATCH {c_q} "
        cluster_collection_r_q, _, _ = self.cluster_is_part_of_collection_relation_mapper.get_cypher_str(
            add_cypher_label=False
        )
        if cluster_id is None:
            cluster_q, cluster_q_id, cluster_merge_a = self.cluster_mapper.get_cypher_str()
        else:
            cluster_q, cluster_q_id, cluster_merge_a = self.cluster_mapper.get_cypher_str({"cluster_id": cluster_id})

        query += f"MATCH {cluster_q}-{cluster_collection_r_q}->({c_id})"
        query += f"DETACH DELETE {cluster_q_id}"
        self._run_tx(tx, query, collection_a | cluster_merge_a)

    def create_indices(self):
        # INDEX
        # SHOW INDEXES
        #
        # CREATE INDEX g_graph_id IF NOT EXISTS FOR (g:Graph) ON (g.graph_id);
        # CREATE TEXT INDEX v_label IF NOT EXISTS FOR (v:VertexLabel) ON (v.label);
        # CREATE INDEX c_collection_id IF NOT EXISTS FOR (c:Collection) ON (c.collection_id);
        # CREATE INDEX cl_cluster_id IF NOT EXISTS FOR (cl:Cluster) ON (cl.cluster_id);

        # DROP INDEX e_label  IF EXISTS;
        # DROP INDEX g_graph_id  IF EXISTS;
        # DROP INDEX v_label  IF EXISTS;
        # DROP INDEX c_collection_id  IF EXISTS;
        # DROP INDEX cl_cluster_id  IF EXISTS;
        with self.driver.session() as session:
            dummy_nodes_relations = [
                "MERGE (collection:Collection {dummy: true})<-[:GRAPH_IS_PART_OF_COLLECTION]-(graph:Graph {dummy: true})",
                "MERGE (graph:Graph {dummy: true})-[:IS_PART_OF_CLUSTER]->(cluster:Cluster {dummy: true})",
                "MERGE (graph:Graph {dummy: true})-[:IS_REPRESENTATIVE_OF]->(cluster:Cluster {dummy: true})",
                "MERGE (graph:Graph {dummy: true})-[:HAS_VERTEX_WITH_LABEL]->(vl:VertexLabel {dummy: true})",
                "MERGE (graph:Graph {dummy: true})-[:HAS_EDGE]->(vl:VertexLabel {dummy: true})",
                "MERGE (collection:Collection {dummy: true})<-[:CLUSTER_IS_PART_OF_COLLECTION]-(cluster:Cluster {dummy: true})",
                "MATCH (collection:Collection {dummy: true})<-[r1:GRAPH_IS_PART_OF_COLLECTION]-(graph:Graph {dummy: true}) DELETE r1;",
                "MATCH (graph:Graph {dummy: true})-[r2:IS_PART_OF_CLUSTER]->(cluster:Cluster {dummy: true}) DELETE r2;",
                "MATCH (graph:Graph {dummy: true})-[r3:IS_REPRESENTATIVE_OF]->(cluster:Cluster {dummy: true}) DELETE r3;",
                "MATCH (graph:Graph {dummy: true})-[r4:HAS_VERTEX_WITH_LABEL]->(vl:VertexLabel {dummy: true}) DELETE r4;",
                "MATCH (graph:Graph {dummy: true})-[r5:HAS_EDGE]->(vl:VertexLabel {dummy: true}) DELETE r5;",
                "MATCH (collection:Collection {dummy: true})<-[r6:CLUSTER_IS_PART_OF_COLLECTION]-(cluster:Cluster {dummy: true}) DELETE r6;",
                "MATCH (n) WHERE n.dummy = true AND NOT (n)--() DELETE n;",
            ]
            indices_queries = [
                "CREATE TEXT INDEX e_label IF NOT EXISTS FOR (e:Edge) ON (e.label);",
                "CREATE INDEX g_graph_id IF NOT EXISTS FOR (g:Graph) ON (g.graph_id);",
                "CREATE TEXT INDEX v_label IF NOT EXISTS FOR (v:VertexLabel) ON (v.label);",
                "CREATE INDEX c_collection_id IF NOT EXISTS FOR (c:Collection) ON (c.collection_id);",
                "CREATE INDEX cl_cluster_id IF NOT EXISTS FOR (cl:Cluster) ON (cl.cluster_id);",
            ]
            for q in dummy_nodes_relations + indices_queries:
                session.run(q)

close()

Close the driver

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def close(self):
    """Close the driver"""
    if self.driver:
        self.driver.close()

delete_graph_collection(graph_collection)

Delete the graph collection in the repository. Delete all nodes of this graph collection.

:param graph_collection: The Graph Collection

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def delete_graph_collection(self, graph_collection: str | GraphCollection):
    """Delete the graph collection in the repository.
    Delete all nodes of this graph collection.

    :param graph_collection: The Graph Collection
    """
    with self.driver.session() as session:
        session.execute_write(
            self._delete_graph_collection,
            collection_id=graph_collection.collection_id
            if isinstance(graph_collection, GraphCollection)
            else graph_collection,
        )

download_graph_collection(collection_id, graph_id=None, cluster_id=None)

Download the Graph Collection with the given ID

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def download_graph_collection(
    self, collection_id: str, graph_id: int | None = None, cluster_id: int | None = None
) -> GraphCollection:
    """Download the Graph Collection with the given ID"""
    with self.driver.session() as session:
        return session.execute_read(
            self._download_graph_collection, collection_id=collection_id, graph_id=graph_id, cluster_id=cluster_id
        )

drop_all()

Drop all nodes in the Repository

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def drop_all(self):
    """Drop all nodes in the Repository"""
    with self.driver.session() as session:
        session.run("MATCH (n) DETACH DELETE (n)")

get_collections()

Get all collections (overview, not data) of the repository

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def get_collections(self) -> list[CollectionInfo]:
    """Get all collections (overview, not data) of the repository"""
    with self.driver.session() as session:
        return session.execute_read(self._get_collections)

get_number_of_clusters(collection_id)

Get the number of clusters of this graph collection

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def get_number_of_clusters(self, collection_id: str) -> int:
    """Get the number of clusters of this graph collection"""
    with self.driver.session() as session:
        return session.execute_read(
            self._get_number_of_clusters,
            collection_id=collection_id,
        )

get_number_of_graphs(collection_id, cluster_id=None)

Get the size of a graph collection, optional filtered by the cluster_id for the size of the cluster

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def get_number_of_graphs(self, collection_id: str, cluster_id: int | None = None) -> int:
    """Get the size of a graph collection, optional filtered by the cluster_id for the size of the cluster"""
    with self.driver.session() as session:
        return session.execute_read(self._get_number_of_graphs, collection_id=collection_id, cluster_id=cluster_id)

set_cluster_attributes(graph_collection, cluster_id)

Saves the attributes for the given cluster

:param graph_collection: The Graph Collection :param cluster_id: Optional: The Cluster Id, if the attributes of a single cluster should be saved

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def set_cluster_attributes(self, graph_collection: GraphCollection, cluster_id: int | None):
    """Saves the attributes for the given cluster

    :param graph_collection: The Graph Collection
    :param cluster_id: Optional: The Cluster Id, if the attributes of a single cluster should be saved
    """
    with self.driver.session() as session:
        return session.execute_write(
            self._set_cluster_attributes,
            graph_collection=graph_collection,
            cluster_id=cluster_id,
        )

set_clusters(graph_collection, cluster_id=None)

Save all cluster information for the given graph_collection

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def set_clusters(self, graph_collection: GraphCollection, cluster_id: int | None = None):
    """Save all cluster information for the given graph_collection"""
    with self.driver.session() as session:
        return session.execute_write(self._set_clusters, graph_collection=graph_collection, cluster_id=cluster_id)

set_graph_metadata(graph_collection, graph_id)

Saves the metadata for the given graph (or all graphs)

:param graph_collection: The Graph Collection :param graph_id: Optional: The Graph Id, if the metadata of a single graph should be saved

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def set_graph_metadata(self, graph_collection: GraphCollection, graph_id: int | None):
    """Saves the metadata for the given graph (or all graphs)

    :param graph_collection: The Graph Collection
    :param graph_id: Optional: The Graph Id, if the metadata of a single graph should be saved
    """
    with self.driver.session() as session:
        return session.execute_write(
            self._set_graph_metadata,
            graph_collection=graph_collection,
            graph_id=graph_id,
        )

upload_graph_collection(graph_collection)

Upload the graph collection Attention: The existing graph collection will be deleted beforehand.

:param graph_collection: The Graph Collection

Source code in src/collaboration_detection/datastructures/neo4jstorage/repository.py
def upload_graph_collection(self, graph_collection: GraphCollection):
    """Upload the graph collection
    Attention: The existing graph collection will be deleted beforehand.

    :param graph_collection: The Graph Collection
    """
    with self.driver.session() as session:
        session.execute_write(self._upload_graph_collection, graph_collection=graph_collection)

Graph Mining

Bases: ABC

Create graphs from event logs. The input events logs are processed and converted using a custom event log converter. Furthermore, this class can be used to create for each original trace individual graphs.

Source code in src/collaboration_detection/graph_mining/graph_miner.py
class GraphMiner(ABC):
    """
    Create graphs from event logs.
    The input events logs are processed and converted using a custom event log converter.
    Furthermore, this class can be used to create for each original trace individual graphs.
    """

    def __init__(self, converter: EventLogConverter | None = None, **kwargs):
        """
        Creates a new Graph Miner

        :param converter: The converter for the preprocessing of the event log
        :param kwargs: Additional parameter for the graph mining algorithm.
        """
        self._converter = DefaultEventLogConverter() if converter is None else converter
        self.kwargs = kwargs

    @property
    def converter(self) -> EventLogConverter:
        """
        Get the current event log converter of this graph miner.
        """
        return self._converter

    def iter_graphs_for_traces(self, event_log: DataFrame) -> Iterator[tuple[Graph, DataFrame]]:
        """
        Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

        :param event_log: The event log
        """
        graph_collection = GraphCollection()
        for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
            new_trace = self._converter.convert_trace(trace)
            yield self._build_graph(new_trace, graph_collection), new_trace

    def get_graphs_for_traces(self, event_log: DataFrame) -> tuple[GraphCollection, list[DataFrame]]:
        """
        This function first converts each trace into a new EventLog
        and creates then creates for each of them a new graph.
        The resulting graphs are stored inside the GraphCollection.

        :param event_log: The event log
        :return: The GraphCollection with the minded graphs
        """
        graph_collection = GraphCollection()
        event_logs = []
        for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
            new_trace = self._converter.convert_trace(trace)
            event_logs.append(new_trace)
            self._build_graph(new_trace, graph_collection)
        return graph_collection, event_logs

    def get_graph(
        self, event_log: DataFrame, graph_collection: GraphCollection | None = None
    ) -> tuple[GraphCollection, list[DataFrame] | DataFrame]:
        """
        This function first converts the event log into a new event log
        and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created.
        The resulting graph(s) is/are stored inside the GraphCollection.

        :param event_log: The event log
        :param graph_collection: A graph collection.
            If no graph collection is provided a new GraphCollection is created.
        :return: The GraphCollection with the minded graph(s)
        """
        if graph_collection is None:
            graph_collection = GraphCollection()
        new_event_logs = self._converter.convert_event_log(event_log)
        if isinstance(new_event_logs, DataFrame):
            self._build_graph(new_event_logs, graph_collection)
        elif isinstance(new_event_logs, list):
            for new_event_log in new_event_logs:
                if isinstance(new_event_log, DataFrame):
                    self._build_graph(new_event_log, graph_collection)

        return graph_collection, new_event_logs

    @abstractmethod
    def _build_graph(self, event_log: DataFrame, graph_collection: GraphCollection) -> Graph: ...

converter property

Get the current event log converter of this graph miner.

__init__(converter=None, **kwargs)

Creates a new Graph Miner

:param converter: The converter for the preprocessing of the event log :param kwargs: Additional parameter for the graph mining algorithm.

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def __init__(self, converter: EventLogConverter | None = None, **kwargs):
    """
    Creates a new Graph Miner

    :param converter: The converter for the preprocessing of the event log
    :param kwargs: Additional parameter for the graph mining algorithm.
    """
    self._converter = DefaultEventLogConverter() if converter is None else converter
    self.kwargs = kwargs

get_graph(event_log, graph_collection=None)

This function first converts the event log into a new event log and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created. The resulting graph(s) is/are stored inside the GraphCollection.

:param event_log: The event log :param graph_collection: A graph collection. If no graph collection is provided a new GraphCollection is created. :return: The GraphCollection with the minded graph(s)

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graph(
    self, event_log: DataFrame, graph_collection: GraphCollection | None = None
) -> tuple[GraphCollection, list[DataFrame] | DataFrame]:
    """
    This function first converts the event log into a new event log
    and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created.
    The resulting graph(s) is/are stored inside the GraphCollection.

    :param event_log: The event log
    :param graph_collection: A graph collection.
        If no graph collection is provided a new GraphCollection is created.
    :return: The GraphCollection with the minded graph(s)
    """
    if graph_collection is None:
        graph_collection = GraphCollection()
    new_event_logs = self._converter.convert_event_log(event_log)
    if isinstance(new_event_logs, DataFrame):
        self._build_graph(new_event_logs, graph_collection)
    elif isinstance(new_event_logs, list):
        for new_event_log in new_event_logs:
            if isinstance(new_event_log, DataFrame):
                self._build_graph(new_event_log, graph_collection)

    return graph_collection, new_event_logs

get_graphs_for_traces(event_log)

This function first converts each trace into a new EventLog and creates then creates for each of them a new graph. The resulting graphs are stored inside the GraphCollection.

:param event_log: The event log :return: The GraphCollection with the minded graphs

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graphs_for_traces(self, event_log: DataFrame) -> tuple[GraphCollection, list[DataFrame]]:
    """
    This function first converts each trace into a new EventLog
    and creates then creates for each of them a new graph.
    The resulting graphs are stored inside the GraphCollection.

    :param event_log: The event log
    :return: The GraphCollection with the minded graphs
    """
    graph_collection = GraphCollection()
    event_logs = []
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        event_logs.append(new_trace)
        self._build_graph(new_trace, graph_collection)
    return graph_collection, event_logs

iter_graphs_for_traces(event_log)

Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

:param event_log: The event log

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def iter_graphs_for_traces(self, event_log: DataFrame) -> Iterator[tuple[Graph, DataFrame]]:
    """
    Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

    :param event_log: The event log
    """
    graph_collection = GraphCollection()
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        yield self._build_graph(new_trace, graph_collection), new_trace

Bases: GraphMiner

A simple graph mining algorithm that takes an event log and converts it into a Directly Follow Graph (DFG). No additional kwargs are required.

Source code in src/collaboration_detection/graph_mining/dfg_graph_miner.py
class DfgGraphMiner(GraphMiner):
    """
    A simple graph mining algorithm that takes an event log and converts it into a Directly Follow Graph (DFG).
    No additional kwargs are required.
    """

    def _build_graph(self, event_log: DataFrame, graph_collection: GraphCollection) -> Graph:
        dfg, _sa, _ea = pm4py.discover_dfg_typed(event_log)
        graph = graph_collection.load_graphs_from_edge_rep(dfg)
        # graph.metadata[M_CASE_ID] = "TODO" # TODO: FIXME if trace is empty!
        if len(graph) == 0:  # TODO: FIXME if trace is empty!
            graph.get_vertex(event_log.iloc[0]['concept:name'])
        return graph

converter property

Get the current event log converter of this graph miner.

__init__(converter=None, **kwargs)

Creates a new Graph Miner

:param converter: The converter for the preprocessing of the event log :param kwargs: Additional parameter for the graph mining algorithm.

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def __init__(self, converter: EventLogConverter | None = None, **kwargs):
    """
    Creates a new Graph Miner

    :param converter: The converter for the preprocessing of the event log
    :param kwargs: Additional parameter for the graph mining algorithm.
    """
    self._converter = DefaultEventLogConverter() if converter is None else converter
    self.kwargs = kwargs

get_graph(event_log, graph_collection=None)

This function first converts the event log into a new event log and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created. The resulting graph(s) is/are stored inside the GraphCollection.

:param event_log: The event log :param graph_collection: A graph collection. If no graph collection is provided a new GraphCollection is created. :return: The GraphCollection with the minded graph(s)

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graph(
    self, event_log: DataFrame, graph_collection: GraphCollection | None = None
) -> tuple[GraphCollection, list[DataFrame] | DataFrame]:
    """
    This function first converts the event log into a new event log
    and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created.
    The resulting graph(s) is/are stored inside the GraphCollection.

    :param event_log: The event log
    :param graph_collection: A graph collection.
        If no graph collection is provided a new GraphCollection is created.
    :return: The GraphCollection with the minded graph(s)
    """
    if graph_collection is None:
        graph_collection = GraphCollection()
    new_event_logs = self._converter.convert_event_log(event_log)
    if isinstance(new_event_logs, DataFrame):
        self._build_graph(new_event_logs, graph_collection)
    elif isinstance(new_event_logs, list):
        for new_event_log in new_event_logs:
            if isinstance(new_event_log, DataFrame):
                self._build_graph(new_event_log, graph_collection)

    return graph_collection, new_event_logs

get_graphs_for_traces(event_log)

This function first converts each trace into a new EventLog and creates then creates for each of them a new graph. The resulting graphs are stored inside the GraphCollection.

:param event_log: The event log :return: The GraphCollection with the minded graphs

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graphs_for_traces(self, event_log: DataFrame) -> tuple[GraphCollection, list[DataFrame]]:
    """
    This function first converts each trace into a new EventLog
    and creates then creates for each of them a new graph.
    The resulting graphs are stored inside the GraphCollection.

    :param event_log: The event log
    :return: The GraphCollection with the minded graphs
    """
    graph_collection = GraphCollection()
    event_logs = []
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        event_logs.append(new_trace)
        self._build_graph(new_trace, graph_collection)
    return graph_collection, event_logs

iter_graphs_for_traces(event_log)

Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

:param event_log: The event log

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def iter_graphs_for_traces(self, event_log: DataFrame) -> Iterator[tuple[Graph, DataFrame]]:
    """
    Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

    :param event_log: The event log
    """
    graph_collection = GraphCollection()
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        yield self._build_graph(new_trace, graph_collection), new_trace

Bases: GraphMiner

Discovers a heuristics net.

The following kwargs parameters can be provided
  • dependency_threshold: Dependency threshold (default: 0.5)
  • and_threshold: AND threshold (default: 0.65)
  • loop_two_threshold: Loop two threshold (default: 0.5)
Source code in src/collaboration_detection/graph_mining/heuristic_graph_miner.py
class HeuristicGraphMiner(GraphMiner):
    """
    Discovers a heuristics net.

    The following kwargs parameters can be provided:
     - dependency_threshold: Dependency threshold (default: 0.5)
     - and_threshold: AND threshold (default: 0.65)
     - loop_two_threshold: Loop two threshold (default: 0.5)
    """

    def _build_graph(self, event_log: DataFrame, graph_collection: GraphCollection) -> Graph:
        dependency_threshold = self.kwargs.get('dependency_threshold', 0.5)
        and_threshold = self.kwargs.get('and_threshold', 0.65)
        loop_two_threshold = self.kwargs.get('and_threshold', 0.5)
        net = discover_heuristics_net(
            event_log,
            dependency_threshold=dependency_threshold,
            and_threshold=and_threshold,
            loop_two_threshold=loop_two_threshold
        )
        graph = graph_collection.load_graph_from_heuristic_net(net)
        # graph.metadata[M_CASE_ID] = case_id # TODO: fixme
        return graph

converter property

Get the current event log converter of this graph miner.

__init__(converter=None, **kwargs)

Creates a new Graph Miner

:param converter: The converter for the preprocessing of the event log :param kwargs: Additional parameter for the graph mining algorithm.

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def __init__(self, converter: EventLogConverter | None = None, **kwargs):
    """
    Creates a new Graph Miner

    :param converter: The converter for the preprocessing of the event log
    :param kwargs: Additional parameter for the graph mining algorithm.
    """
    self._converter = DefaultEventLogConverter() if converter is None else converter
    self.kwargs = kwargs

get_graph(event_log, graph_collection=None)

This function first converts the event log into a new event log and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created. The resulting graph(s) is/are stored inside the GraphCollection.

:param event_log: The event log :param graph_collection: A graph collection. If no graph collection is provided a new GraphCollection is created. :return: The GraphCollection with the minded graph(s)

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graph(
    self, event_log: DataFrame, graph_collection: GraphCollection | None = None
) -> tuple[GraphCollection, list[DataFrame] | DataFrame]:
    """
    This function first converts the event log into a new event log
    and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created.
    The resulting graph(s) is/are stored inside the GraphCollection.

    :param event_log: The event log
    :param graph_collection: A graph collection.
        If no graph collection is provided a new GraphCollection is created.
    :return: The GraphCollection with the minded graph(s)
    """
    if graph_collection is None:
        graph_collection = GraphCollection()
    new_event_logs = self._converter.convert_event_log(event_log)
    if isinstance(new_event_logs, DataFrame):
        self._build_graph(new_event_logs, graph_collection)
    elif isinstance(new_event_logs, list):
        for new_event_log in new_event_logs:
            if isinstance(new_event_log, DataFrame):
                self._build_graph(new_event_log, graph_collection)

    return graph_collection, new_event_logs

get_graphs_for_traces(event_log)

This function first converts each trace into a new EventLog and creates then creates for each of them a new graph. The resulting graphs are stored inside the GraphCollection.

:param event_log: The event log :return: The GraphCollection with the minded graphs

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graphs_for_traces(self, event_log: DataFrame) -> tuple[GraphCollection, list[DataFrame]]:
    """
    This function first converts each trace into a new EventLog
    and creates then creates for each of them a new graph.
    The resulting graphs are stored inside the GraphCollection.

    :param event_log: The event log
    :return: The GraphCollection with the minded graphs
    """
    graph_collection = GraphCollection()
    event_logs = []
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        event_logs.append(new_trace)
        self._build_graph(new_trace, graph_collection)
    return graph_collection, event_logs

iter_graphs_for_traces(event_log)

Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

:param event_log: The event log

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def iter_graphs_for_traces(self, event_log: DataFrame) -> Iterator[tuple[Graph, DataFrame]]:
    """
    Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

    :param event_log: The event log
    """
    graph_collection = GraphCollection()
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        yield self._build_graph(new_trace, graph_collection), new_trace

Bases: GraphMiner

This algorithm discovers a collaboration instance graph.

The following kwargs parameters can be provided
  • activity_classifier: List of additional attributes for the concept name of the activity nodes. default: []
  • relation_attributes: List of attribute names, which are used as object nodes that are connected to the activities. default = []
  • activity_delimiter: Delimiter of the activity classifier attributes. default = " -- "
  • create_object_nodes: If true, create the object nodes; if false, just simulate the object nodes and only create the activty nodes; default: True
  • override_labels_of_relation_attributes: If true, the labels of the object nodes are overridden with the object type (e.g. org:resource, spm:sdid, ..); If a set is provided, only the attributes defined in this sets are used to override the object node labeles with its value. Default: False
  • use_object_type_in_label: Create the label of the object nodes with the object type as part of label
Source code in src/collaboration_detection/graph_mining/collaboration_instance_graph_miner.py
class CollaborationInstanceGraphMiner(GraphMiner):
    """
    This algorithm discovers a collaboration instance graph.

    The following kwargs parameters can be provided:
     - activity_classifier: List of additional attributes for the concept name of the activity nodes.
        default: []
     - relation_attributes: List of attribute names, which are used as object nodes that
       are connected to the activities. default = []
     - activity_delimiter: Delimiter of the activity classifier attributes. default = " -- "
     - create_object_nodes: If true, create the object nodes; if false,
        just simulate the object nodes and only create the activty nodes; default: True
     - override_labels_of_relation_attributes: If true, the labels of the object nodes are overridden
       with the object type (e.g. org:resource, spm:sdid, ..); If a set is provided, only the attributes
       defined in this sets are used to override the object node labeles with its value.
       Default: False
     - use_object_type_in_label: Create the label of the object nodes with the object type as part of label
    """

    def _build_graph(self, event_log: DataFrame, graph_collection: GraphCollection) -> Graph:
        new_graph = graph_collection.new_graph()
        activity_classifier: list[str] = self.kwargs.get("activity_classifier", [])
        activity_delimiter: str = self.kwargs.get("activity_delimiter", " -- ")
        create_object_nodes: bool = self.kwargs.get("create_object_nodes", True)
        override_labels_of_relation_attributes: bool | set[str] = self.kwargs.get(
            "override_labels_of_relation_attributes", False
        )
        event_log = ActivityJoinerConverter(
            activity_classifier=["concept:name"] + activity_classifier, activity_delimiter=activity_delimiter
        ).convert_event_log(event_log)
        relation_attributes = self.kwargs.get("relation_attributes", [])
        last_activity_node: Vertex | None = None
        use_object_type_in_label: bool = self.kwargs.get("use_object_type_in_label", True)

        activity_node_mapping: dict[tuple[str, ...], Vertex] = {}
        for _, event in event_log.iterrows():
            # Create object vertices, and build mapping key
            att_vertex_labels = []
            for attr in relation_attributes:
                att_vertex_label = self._get_attr_label(event, attr, use_object_type_in_label)
                if att_vertex_label != "":
                    if create_object_nodes:
                        _ = new_graph.get_vertex(
                            att_vertex_label,
                            metadata={"v_type": attr},
                        )
                    att_vertex_labels.append(att_vertex_label)
            mapping_key = tuple([event["concept:name"]] + att_vertex_labels)

            # Get the current activity node / or create it
            current_activity_node = activity_node_mapping.get(mapping_key)
            if current_activity_node is None:
                current_activity_node = new_graph.get_vertex(
                    event["concept:name"],
                    force_create=True,
                    metadata={"v_type": "activity"},
                )
                activity_node_mapping[mapping_key] = current_activity_node

                # Create edges to the related object vertices
                if create_object_nodes:
                    for attr in relation_attributes:
                        att_vertex_label = self._get_attr_label(event, attr, use_object_type_in_label)
                        if att_vertex_label != "":
                            att_vertex = new_graph.get_vertex(att_vertex_label)
                            current_activity_node.add_edge(att_vertex, directed=True)

            # Create edge from previous event (follow relation)
            if last_activity_node is not None and not last_activity_node.has_edge(current_activity_node):
                new_graph.add_edge(
                    last_activity_node,
                    current_activity_node,
                    directed=True,
                )
            last_activity_node = current_activity_node
        # Override object node labels if requested
        if isinstance(override_labels_of_relation_attributes, bool) and override_labels_of_relation_attributes:
            override_labels_of_object_nodes(new_graph, relation_attributes)
        if isinstance(override_labels_of_relation_attributes, set) and len(override_labels_of_relation_attributes) >= 1:
            override_labels_of_object_nodes(new_graph, override_labels_of_relation_attributes)
        return new_graph

    def _get_attr_label(self, event, attr, use_object_type_in_label=True):
        event_label = str(event.get(attr, ""))
        if event_label == "" or event_label.lower() == "nan" or event_label is None or event_label == "None":
            return ""
        if use_object_type_in_label:
            return attr + ": " + event_label
        else:
            return event_label

converter property

Get the current event log converter of this graph miner.

__init__(converter=None, **kwargs)

Creates a new Graph Miner

:param converter: The converter for the preprocessing of the event log :param kwargs: Additional parameter for the graph mining algorithm.

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def __init__(self, converter: EventLogConverter | None = None, **kwargs):
    """
    Creates a new Graph Miner

    :param converter: The converter for the preprocessing of the event log
    :param kwargs: Additional parameter for the graph mining algorithm.
    """
    self._converter = DefaultEventLogConverter() if converter is None else converter
    self.kwargs = kwargs

get_graph(event_log, graph_collection=None)

This function first converts the event log into a new event log and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created. The resulting graph(s) is/are stored inside the GraphCollection.

:param event_log: The event log :param graph_collection: A graph collection. If no graph collection is provided a new GraphCollection is created. :return: The GraphCollection with the minded graph(s)

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graph(
    self, event_log: DataFrame, graph_collection: GraphCollection | None = None
) -> tuple[GraphCollection, list[DataFrame] | DataFrame]:
    """
    This function first converts the event log into a new event log
    and creates then a new graph. If the converter returns multiple sublogs, multiple graphs are created.
    The resulting graph(s) is/are stored inside the GraphCollection.

    :param event_log: The event log
    :param graph_collection: A graph collection.
        If no graph collection is provided a new GraphCollection is created.
    :return: The GraphCollection with the minded graph(s)
    """
    if graph_collection is None:
        graph_collection = GraphCollection()
    new_event_logs = self._converter.convert_event_log(event_log)
    if isinstance(new_event_logs, DataFrame):
        self._build_graph(new_event_logs, graph_collection)
    elif isinstance(new_event_logs, list):
        for new_event_log in new_event_logs:
            if isinstance(new_event_log, DataFrame):
                self._build_graph(new_event_log, graph_collection)

    return graph_collection, new_event_logs

get_graphs_for_traces(event_log)

This function first converts each trace into a new EventLog and creates then creates for each of them a new graph. The resulting graphs are stored inside the GraphCollection.

:param event_log: The event log :return: The GraphCollection with the minded graphs

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def get_graphs_for_traces(self, event_log: DataFrame) -> tuple[GraphCollection, list[DataFrame]]:
    """
    This function first converts each trace into a new EventLog
    and creates then creates for each of them a new graph.
    The resulting graphs are stored inside the GraphCollection.

    :param event_log: The event log
    :return: The GraphCollection with the minded graphs
    """
    graph_collection = GraphCollection()
    event_logs = []
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        event_logs.append(new_trace)
        self._build_graph(new_trace, graph_collection)
    return graph_collection, event_logs

iter_graphs_for_traces(event_log)

Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

:param event_log: The event log

Source code in src/collaboration_detection/graph_mining/graph_miner.py
def iter_graphs_for_traces(self, event_log: DataFrame) -> Iterator[tuple[Graph, DataFrame]]:
    """
    Iter all traces, convert them into a new EventLog and yield a graph for each of these traces.

    :param event_log: The event log
    """
    graph_collection = GraphCollection()
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        new_trace = self._converter.convert_trace(trace)
        yield self._build_graph(new_trace, graph_collection), new_trace

Preprocessing Converters

Bases: ABC

Transform an event log into a new event log where the traces and/or events (and/or their attributes) are preprocessed / converted based on the concrete implementation of the converter.

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
class EventLogConverter(ABC):
    """
    Transform an event log into a new event log where the traces and/or events
    (and/or their attributes) are preprocessed / converted based on the concrete implementation of the converter.
    """

    @abstractmethod
    def convert_event_log(self, event_log: DataFrame) -> DataFrame | list[DataFrame]:
        """
        Converts an event log into one or more new event log(s).

        :param event_log: The input event log
        :return: The converted event log(s)
        """
        raise NotImplementedError

    @abstractmethod
    def is_sub_log_converter(self) -> bool:
        """
        Does the converter returns multiple sub-logs?

        :return: True if convert_event_log returns multiple (sub-)logs
        """
        raise NotImplementedError

    @abstractmethod
    def convert_trace(self, trace: DataFrame) -> DataFrame:
        """
        Converts a trace into one (OR MORE) new trace(s).

        :param trace: The input trace
        :return: The converted trace or a list of traces
        """
        raise NotImplementedError

    @abstractmethod
    def convert_event(self, event: Series) -> Series:
        """
        Converts an event into a new event.

        :param event: The input event
        :return: The converted event
        """
        raise NotImplementedError

    def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
        """
        Iterates over an event log and yields all converted traces.

        :param event_log: The input event log
        """
        for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
            yield self.convert_trace(trace)

convert_event(event) abstractmethod

Converts an event into a new event.

:param event: The input event :return: The converted event

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
@abstractmethod
def convert_event(self, event: Series) -> Series:
    """
    Converts an event into a new event.

    :param event: The input event
    :return: The converted event
    """
    raise NotImplementedError

convert_event_log(event_log) abstractmethod

Converts an event log into one or more new event log(s).

:param event_log: The input event log :return: The converted event log(s)

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
@abstractmethod
def convert_event_log(self, event_log: DataFrame) -> DataFrame | list[DataFrame]:
    """
    Converts an event log into one or more new event log(s).

    :param event_log: The input event log
    :return: The converted event log(s)
    """
    raise NotImplementedError

convert_trace(trace) abstractmethod

Converts a trace into one (OR MORE) new trace(s).

:param trace: The input trace :return: The converted trace or a list of traces

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
@abstractmethod
def convert_trace(self, trace: DataFrame) -> DataFrame:
    """
    Converts a trace into one (OR MORE) new trace(s).

    :param trace: The input trace
    :return: The converted trace or a list of traces
    """
    raise NotImplementedError

is_sub_log_converter() abstractmethod

Does the converter returns multiple sub-logs?

:return: True if convert_event_log returns multiple (sub-)logs

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
@abstractmethod
def is_sub_log_converter(self) -> bool:
    """
    Does the converter returns multiple sub-logs?

    :return: True if convert_event_log returns multiple (sub-)logs
    """
    raise NotImplementedError

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: EventLogConverter

The DefaultEventLogConverter converts all traces and events as they are (no modifications). Can be used as base class for inheritance.

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
class DefaultEventLogConverter(EventLogConverter):
    """
    The DefaultEventLogConverter converts all traces and events as they are (no modifications).
    Can be used as base class for inheritance.
    """

    def is_sub_log_converter(self) -> bool:
        return False

    def convert_event_log(self, event_log: DataFrame) -> DataFrame | list[DataFrame]:
        new_log = (event_log
                   .groupby(CASE_CONCEPT_NAME, group_keys=False)[event_log.columns]
                   .apply(self.convert_trace, include_groups=False))
        new_log.reset_index(drop=True, inplace=True)
        return new_log

    def convert_trace(self, trace: DataFrame) -> DataFrame:
        new_trace = trace.apply(self.convert_event, axis=1)
        return new_trace

    def convert_event(self, event: Series) -> Series:
        new_event = event.copy(deep=True)
        return new_event

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: DefaultEventLogConverter

The ActivityJoinerConverter converts the concept:name of all events by joining the attributes provided by activity_classifier. All other attributes are untouched.

Source code in src/collaboration_detection/preprocessing/event_log_converter/activity_joiner_converter.py
class ActivityJoinerConverter(DefaultEventLogConverter):
    """
    The ActivityJoinerConverter converts the `concept:name` of all events
    by joining the attributes provided by `activity_classifier`. All other attributes are untouched.
    """

    def __init__(self, activity_classifier: list[str] | None = None, activity_delimiter=" / "):
        if activity_classifier is None:
            activity_classifier = ['concept:name']
        self.activity_classifier = activity_classifier
        self.activity_delimiter = activity_delimiter

    def convert_event_log(self, event_log: DataFrame) -> DataFrame:
        new_event_log = event_log.copy(deep=True)
        new_event_log['concept:name'] = (new_event_log[self.activity_classifier]
                                         .agg(self.join_with_default, axis=1))
        return new_event_log

    def join_with_default(self, args):
        return self.activity_delimiter.join(args.dropna())

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: DefaultEventLogConverter

The trace split converter convert a trace from the event log into a new trace with modified case IDs. The new case IDs are created by appending a unique identifier to the original case ID for each trace. The unique identifier is created by concatenating the values (the category codes) for the attributes specified in the split_attributes list. If the split_attributes list is empty, the case IDs are not modified.

Source code in src/collaboration_detection/preprocessing/event_log_converter/trace_split_converter.py
class TraceSplitConverter(DefaultEventLogConverter):
    """
    The trace split converter convert a trace from the event log into a new trace with modified case IDs.
    The new case IDs are created by appending a unique identifier to the original case ID for each trace.
    The unique identifier is created by concatenating the values (the category codes) for the attributes specified
    in the `split_attributes` list. If the `split_attributes` list is empty, the case IDs are not modified.
    """

    def __init__(self, split_attributes: list[str]):
        self.split_attributes = split_attributes

    def convert_event_log(self, event_log: DataFrame) -> DataFrame | list[DataFrame]:
        return self._create_subtraces(event_log.copy())

    def convert_trace(self, trace: DataFrame) -> DataFrame:
        return self._create_subtraces(trace.copy())

    def _create_subtraces(self, traces: DataFrame) -> DataFrame:
        if len(self.split_attributes) == 0:
            return traces
        cat_columns = [f"{attribute}_CAT" for attribute in self.split_attributes]
        for attribute, cat_attribute in zip(self.split_attributes, cat_columns):
            traces[cat_attribute] = traces[attribute].astype("category").cat.codes
        traces[CASE_CONCEPT_NAME] = (
            traces[CASE_CONCEPT_NAME]
            + "_"
            + traces[cat_columns].apply(lambda row: "_".join(row.values.astype(str)), axis=1)
        )
        traces.drop(columns=cat_columns, inplace=True)
        traces.sort_values([CASE_CONCEPT_NAME, "time:timestamp"], inplace=True)
        return traces

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: EventLogConverter

The CombinedConverter combines multiple event log converters into a single converter. The convert_event_log, convert_trace, and convert_event methods apply the corresponding method of each converter in the given order to the input data.

Source code in src/collaboration_detection/preprocessing/event_log_converter/combined_converter.py
class CombinedConverter(EventLogConverter):
    """
    The CombinedConverter combines multiple event log converters into a single converter.
    The `convert_event_log`, `convert_trace`, and `convert_event`
    methods apply the corresponding method of each converter in the given order to the input data.
    """

    def __init__(self, converter_list: list[EventLogConverter]):
        self.converter_list = converter_list

    def convert_event_log(self, event_log: DataFrame) -> DataFrame | list[DataFrame]:
        logs = [event_log]
        for conv in self.converter_list:
            new_logs: list[DataFrame] = []
            for log in logs:
                new_log = conv.convert_event_log(log)
                if isinstance(new_log, list) or conv.is_sub_log_converter():
                    for n_log in new_log:
                        if isinstance(n_log, DataFrame):
                            new_logs.append(n_log)
                elif isinstance(new_log, DataFrame):
                    new_logs.append(new_log)
            logs = new_logs
        return logs if len(logs) > 1 else logs[0]

    def is_sub_log_converter(self) -> bool:
        return any(c.is_sub_log_converter() for c in self.converter_list)

    def convert_trace(self, trace: DataFrame) -> DataFrame:
        for conv in self.converter_list:
            trace = conv.convert_trace(trace)
        return trace

    def convert_event(self, event: Series) -> Series:
        for conv in self.converter_list:
            event = conv.convert_event(event)
        return event

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: TraceSplitConverter

Convert an event log into a list of sublogs. The split_attributes list is used to split each existing trace into multiple subtraces which are then are used to create the sublogs. Each sublog contains a group of related (sub)traces, where the trace are considered as related if their events have overlapping timestamps and share common values for the attributes specified in the similarity_attributes list. The timedelta parameter defines how many seconds two events of two traces can be apart from each other.

Source code in src/collaboration_detection/preprocessing/event_log_converter/sublog_converter.py
class SublogConverter(TraceSplitConverter):
    """
    Convert an event log into a list of sublogs. The `split_attributes` list is used to split each existing trace into
    multiple subtraces which are then are used to create the sublogs.
    Each sublog contains a group of related (sub)traces, where the trace are considered as related if their events have
    overlapping timestamps and share common values for the attributes specified in the `similarity_attributes` list.
    The `timedelta` parameter defines how many seconds two events of two traces can be apart from each other.
    """

    def __init__(self, split_attributes: list[str], similarity_attributes: list[str], timedelta_s: int = 0):
        super().__init__(split_attributes)
        self.similarity_attributes = similarity_attributes
        self.check_timeframe_overlap = True
        self.timedelta = datetime.timedelta(seconds=timedelta_s)

    def is_sub_log_converter(self) -> bool:
        return True

    def convert_event_log(self, event_log: DataFrame) -> list[DataFrame]:
        # with Pool(min(cpu_count() - 1, 1)) as p:
        #    sublogs = p.map(self._convert_event_log, self.iter_converted_traces(event_log))
        sublogs = map(self._convert_event_log, self.iter_converted_traces(event_log))
        return [sublog for sublog_list in sublogs for sublog in sublog_list]

    def _convert_event_log(self, sub_traces: DataFrame) -> list[DataFrame]:
        cases = get_event_log_from_dataframe(sub_traces)

        # 1. Build a relationship matrix
        num_cases = len(cases)
        # matrix = [[False] * num_cases for _ in range(num_cases)]
        matrix = np.zeros(num_cases**2).reshape((num_cases, num_cases))

        # iterate over the grouped dataframe to create the matrix
        for i, trace1 in enumerate(cases):
            for j, trace2 in enumerate(cases):
                if j >= i:
                    matrix[i, j] = self._trace_are_related(trace1, trace2)
                    matrix[j, i] = matrix[i][j]
                else:
                    break

        # 2. Add related cases to new sublogs
        # Create a new sublog for each group of related cases, including cases that are indirectly related,
        # and ensure that each case is only added to one sublog.
        # It does this by keeping track of which cases have already been added to a sublog,
        # and skipping those cases in the BFS algorithm.
        new_sublogs: list[DataFrame] = []
        visited_cases = set()
        for i, trace1 in enumerate(cases):
            # Skip cases that have already been added to a sublog
            if i in visited_cases:
                continue
            # Initialize the queue with the current case
            queue = [(i, trace1)]
            visited = {i}
            sublog_events = []
            while queue:
                # Get the next case in the queue
                case_index, case_trace = queue.pop(0)
                sublog_events.extend(case_trace)
                # Add all indirectly related cases to the queue
                for j, trace2 in enumerate(cases):
                    if matrix[case_index][j] == 1 and j not in visited:
                        queue.append((j, trace2))
                        visited.add(j)
            # Add the traces to a new sublog if there are any related cases
            if len(sublog_events) > 0:
                new_sublogs.append(DataFrame.from_records(sublog_events))
                visited_cases.update(visited)

        return new_sublogs

    def _trace_are_related(self, trace1: TraceType, trace2: TraceType) -> int:
        # 1. check: are the timestamps overlapping?
        if self.check_timeframe_overlap:
            min_timestamp_1 = min(e["time:timestamp"] for e in trace1)
            max_timestamp_1 = max(e["time:timestamp"] for e in trace1)
            min_timestamp_2 = min(e["time:timestamp"] for e in trace2)
            max_timestamp_2 = max(e["time:timestamp"] for e in trace2)
            if (min_timestamp_1 > max_timestamp_2 + self.timedelta) or (
                max_timestamp_1 + self.timedelta < min_timestamp_2
            ):
                return 0
        # 2. check if the similarity attributes in some events are the same?
        for s_attr in self.similarity_attributes:
            sublog_attrs = {e[s_attr] for e in trace1}
            trace_attrs = {e[s_attr] for e in trace2}
            if not sublog_attrs.intersection(trace_attrs):
                return 0
        return 1

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: DefaultEventLogConverter

This converter adds a new pseudo-event at the beginning and end of the trace, depending on the values of the add_pseudo_start_event and add_pseudo_end_event variables. The timestamp of the events is based on the first/last event -/+ one second.

The new events are created using the attributes_start_event and attributes_end_event dictionaries and are added to the trace.

Source code in src/collaboration_detection/preprocessing/event_log_converter/add_pseudo_event_converter.py
class AddPseudoEventConverter(DefaultEventLogConverter):
    """
    This converter adds a new pseudo-event at the beginning and end of the trace,
    depending on the values of the `add_pseudo_start_event` and `add_pseudo_end_event` variables.
    The timestamp of the events is based on the first/last event -/+ one second.

    The new events are created using the `attributes_start_event` and `attributes_end_event` dictionaries
    and are added to the trace.
    """

    def __init__(self, add_pseudo_start_event=True, add_pseudo_end_event=True,
                 attributes_start_event: dict[str, Any] | None = None,
                 attributes_end_event: dict[str, Any] | None = None):
        self.add_pseudo_start_event = add_pseudo_start_event
        self.add_pseudo_end_event = add_pseudo_end_event
        if attributes_start_event is None:
            self.attributes_start_event = {"concept:name": "Start"}
        else:
            self.attributes_start_event = {"concept:name": "Start"} | attributes_start_event
        if attributes_end_event is None:
            self.attributes_end_event = {"concept:name": "End"}
        else:
            self.attributes_end_event = {"concept:name": "End"} | attributes_end_event

    def convert_trace(self, trace: DataFrame) -> DataFrame:
        if self.add_pseudo_start_event:
            attributes_start_event = self.attributes_start_event
            attributes_start_event["time:timestamp"] = trace['time:timestamp'].min() - datetime.timedelta(seconds=1)
            attributes_start_event[CASE_CONCEPT_NAME] = trace[CASE_CONCEPT_NAME].iloc[0]
            trace = pd.concat([pd.Series(attributes_start_event).to_frame().T, trace])
        if self.add_pseudo_end_event:
            attributes_end_event = self.attributes_end_event
            attributes_end_event[CASE_CONCEPT_NAME] = trace[CASE_CONCEPT_NAME].iloc[0]
            attributes_end_event["time:timestamp"] = trace['time:timestamp'].max() + datetime.timedelta(seconds=1)
            trace = pd.concat([trace, pd.Series(attributes_end_event).to_frame().T])
        return trace

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: DefaultEventLogConverter

Merges all traces of the event log into a single trace by setting the CASE_CONCEPT_NAME to the value provided by the function case_id_provider or by a fixed value defined by new_case_id.

Source code in src/collaboration_detection/preprocessing/event_log_converter/trace_merger_converter.py
class TraceMergerConverter(DefaultEventLogConverter):
    """
    Merges all traces of the event log into a single trace by setting the `CASE_CONCEPT_NAME` to the value
    provided by the function `case_id_provider` or by a fixed value defined by `new_case_id`.
    """

    def __init__(self, *, new_case_id="NEW_DEFAULT_CASE", case_id_provider: Callable[[], str] | None = None):
        self.case_id_provider = case_id_provider
        self.new_case_id = new_case_id

    def convert_event_log(self, event_log: DataFrame) -> DataFrame:
        new_log = event_log.copy()
        new_log[CASE_CONCEPT_NAME] = self.new_case_id if self.case_id_provider is None else self.case_id_provider()
        new_log.sort_values(["time:timestamp"], inplace=True)
        new_log.reset_index(drop=True, inplace=True)
        return new_log

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Bases: DefaultEventLogConverter

The AddCountAttributeInTraceConverter adds count-based attributes to specified columns in a trace DataFrame. The parameter columns_with_value_prefix (Dict[str, str]) defines a dictionary specifying columns and their corresponding value prefixes. The value prefixes will be used to create a new attribute for each unique value in the specified columns. The parameter column_prefix defines a prefix that will be added to the newly created columns.

Source code in src/collaboration_detection/preprocessing/event_log_converter/add_count_attribute_in_traces_converter.py
class AddCountAttributeInTraceConverter(DefaultEventLogConverter):
    """
    The AddCountAttributeInTraceConverter adds count-based attributes to specified columns in a trace DataFrame.
    The parameter `columns_with_value_prefix` (Dict[str, str]) defines a dictionary specifying columns and their
    corresponding value prefixes. The value prefixes will be used to create a new attribute for each unique value
    in the specified columns.
    The parameter `column_prefix` defines a prefix that will be added to the newly created columns.
    """

    def __init__(self, columns_with_value_prefix: dict[str, str], column_prefix="counted_"):
        self.columns_with_value_prefix = columns_with_value_prefix
        self.column_prefix = column_prefix

    def convert_trace(self, trace: DataFrame) -> DataFrame:
        new_trace = trace.copy(deep=True)
        for column, value_prefix in self.columns_with_value_prefix.items():
            mask = new_trace[column].notna()
            new_trace[self.column_prefix + column] = value_prefix + new_trace.loc[mask].groupby(
                column, sort=False
            ).ngroup().add(1).astype(str)
        return new_trace

iter_converted_traces(event_log)

Iterates over an event log and yields all converted traces.

:param event_log: The input event log

Source code in src/collaboration_detection/preprocessing/event_log_converter/event_log_converter.py
def iter_converted_traces(self, event_log: DataFrame) -> Iterable[DataFrame]:
    """
    Iterates over an event log and yields all converted traces.

    :param event_log: The input event log
    """
    for _, trace in event_log.groupby(CASE_CONCEPT_NAME, group_keys=False):
        yield self.convert_trace(trace)

Frequent Subgraph Mining

execute_gspan(data, result=None, sup=10, min_node=3, max_node=10, remove_data_graphs=True, variant=GSpanVariant.GSPAN_JAVA_PARSEMIS)

Executes the gspan algorithm using either a Java or Rust implementation.

The implementation binary or jar file should be provided in the bin folder. Download the Java gspan jar from: GitHub gSpan.Java <https://github.com/joleaf/gSpan.Java/tree/master/target>_ GitHub parsemis <https://github.com/timtadh/parsemis>_ (supports directed edges) (preffed version) and GitHub parsemis wrapper (jar file) <https://github.com/tomkdickinson/parsemis_wrapper/blob/master/parsemis/parsemis.jar>_

The data parameter must be provided. The output is stored in the Graph Collection.

:param data: File path (str) of the graph data set / or GraphCollection :param result: File path of the result file :param sup: Minimum support :param min_node: Minimum number of nodes for each sub-graph :param max_node: Maximum number of nodes for each sub-graph :param remove_data_graphs: Only relevant, if type(data)==GraphCollection: Remove all other graphs before adding the sub graphs :param variant: Define the GSpanVariant (java or rust implementation) :param ignore_directed: Define if gspan should ignore the direction of edges

Source code in src/collaboration_detection/subgraph_mining/gspan/gspan_wrapper.py
def execute_gspan(
    data: str | GraphCollection,
    result=None,
    sup=10,
    min_node=3,
    max_node=10,
    remove_data_graphs=True,
    variant: GSpanVariant = GSpanVariant.GSPAN_JAVA_PARSEMIS,
):
    """
    Executes the gspan algorithm using either a Java or Rust implementation.

    The implementation binary or jar file should be provided in the `bin` folder.
    Download the Java gspan jar from:
    `GitHub gSpan.Java <https://github.com/joleaf/gSpan.Java/tree/master/target>`_
    `GitHub parsemis <https://github.com/timtadh/parsemis>`_ (supports directed edges) (preffed version)
    and `GitHub parsemis wrapper (jar file) <https://github.com/tomkdickinson/parsemis_wrapper/blob/master/parsemis/parsemis.jar>`_

    The data parameter must be provided. The output is stored in the Graph Collection.

    :param data: File path (str) of the graph data set / or GraphCollection
    :param result: File path of the result file
    :param sup: Minimum support
    :param min_node: Minimum number of nodes for each sub-graph
    :param max_node: Maximum number of nodes for each sub-graph
    :param remove_data_graphs: Only relevant, if type(data)==GraphCollection:
      Remove all other graphs before adding the sub graphs
    :param variant: Define the GSpanVariant (java or rust implementation)
    :param ignore_directed: Define if gspan should ignore the direction of edges
    """
    tmp_data_file = None
    if isinstance(data, GraphCollection):
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".lg") as tmp_data_file:
            data.save_str_rep(tmp_data_file)
            data_path = tmp_data_file.name
    else:
        data_path = data
    if result is None:
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".lg") as tpm_result_file:
            result_file_path = tpm_result_file.name
    else:
        result_file_path = result

    bin_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "bin")
    if variant == GSpanVariant.GSPAN_RUST:
        rust_binary = "gspan-darwin-arm64"
        if platform.system() == "Linux":
            rust_binary = f"gspan-linux-{'arm' if 'arm' in platform.uname() else 'amd'}64"
        elif platform.system() == "Windows":
            rust_binary = "gspan-amd64.exe"
        elif platform.system() == "Darwin":
            rust_binary = f"gspan-darwin-{'arm' if 'arm' in platform.uname() else 'amd'}64"
        calling_gspan = [
            os.path.join(bin_folder, rust_binary),
            "--input",
            data_path,
            "--output",
            result_file_path,
            "--min-vertices",
            str(min_node),
            "--max-vertices",
            str(max_node),
            "--support",
            str(sup),
        ]
        calling_gspan.append("--directed")
        subprocess.run(calling_gspan, check=False)
    elif variant == GSpanVariant.GSPAN_JAVA:
        calling_gspan = [
            "java",
            "-jar",
            "-Xmx10g",
            os.path.join(bin_folder, "gSpan.Java-1.2.jar"),
            "--data",
            data_path,
            "--result",
            result_file_path,
            "--min-node",
            str(min_node),
            "--max-node",
            str(max_node),
            "--sup",
            str(sup),
        ]
        calling_gspan.append("--graph-type")
        calling_gspan.append("directed")
        subprocess.run(calling_gspan, check=False)
    elif variant == GSpanVariant.GSPAN_JAVA_PARSEMIS or variant is None:
        calling_gspan = [
            "java",
            "-jar",
            "-Xmx10g",
            os.path.join(bin_folder, "parsemis.jar"),
            f"--graphFile={data_path}",
            f"--outputFile={result_file_path}",
            f"--minimumNodeCount={min_node}",
            f"--maximumNodeCount={max_node}",
            f"--minimumFrequency={sup}",
        ]
        subprocess.run(calling_gspan, check=False)  # tmp_data_file was handled by context manager

    if isinstance(data, GraphCollection):
        if remove_data_graphs:
            data.clean(data.label_list)

        load_fsm_result_into_graph_collection(result_file_path, data)
    if result is None:
        os.remove(result_file_path)
    if isinstance(data, GraphCollection):
        os.remove(data_path)

execute_subdue(data, result=None, min_node=3, max_node=10, iterations=0, nsubs=3, beam=10, limit=10, remove_data_graphs=True)

Executes the subdue algorithm using a standard C implementation. The subdue module must be downloaded and built. The data parameter must be provided. The output is stored in the Graph Collection.

:param data: File path (str) of the graph data set / or GraphCollection :param result: File path of the result file :param min_node: Minimum number of nodes for each sub-graph :param max_node: Maximum number of nodes for each sub-graph :param iterations: Number of iterations :param nsubs: Number of substructures that should be found :param beam: Beam width for the search :param limit: Limit on the number of substructures :param remove_data_graphs: Only relevant, if type(data)==GraphCollection: Remove all other graphs before adding the sub graphs

Source code in src/collaboration_detection/subgraph_mining/subdue/subdue_wrapper.py
def execute_subdue(
    data: str | GraphCollection,
    result=None,
    min_node=3,
    max_node=10,
    iterations=0,
    nsubs=3,
    beam=10,
    limit=10,
    remove_data_graphs=True,
):
    """
    Executes the subdue algorithm using a standard C implementation.
    The subdue module must be downloaded and built.
    The data parameter must be provided. The output is stored in the Graph Collection.

    :param data: File path (str) of the graph data set / or GraphCollection
    :param result: File path of the result file
    :param min_node: Minimum number of nodes for each sub-graph
    :param max_node: Maximum number of nodes for each sub-graph
    :param iterations: Number of iterations
    :param nsubs: Number of substructures that should be found
    :param beam: Beam width for the search
    :param limit: Limit on the number of substructures
    :param remove_data_graphs: Only relevant, if type(data)==GraphCollection:
      Remove all other graphs before adding the sub graphs
    """
    subdue_path = subdue_check_install()
    tmp_data_file = None
    if isinstance(data, GraphCollection):
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp_data_file:
            data.save_str_rep(tmp_data_file, variant="subdue")
            data_path = tmp_data_file.name
    else:
        data_path = data
    if result is None:
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tpm_result_file:
            result_file_path = tpm_result_file.name
    else:
        result_file_path = result

    calling_subdue = [
        subdue_path,
        "-minsize",
        str(min_node),
        "-beam",
        str(beam),
        "-maxsize",
        str(max_node),
        "-nsubs",
        str(nsubs),
        "-limit",
        str(limit),
        "-iterations",
        str(iterations),
        "-out",
        result_file_path,
        data_path,
    ]
    subprocess.run(calling_subdue, check=False)
    # tmp_data_file was handled by context manager

    if isinstance(data, GraphCollection):
        if remove_data_graphs:
            data.clean(data.label_list)

        data.load_subdue_results_from_file(result_file_path)
    if result is None:
        os.remove(result_file_path)
    if isinstance(data, GraphCollection):
        os.remove(data_path)

Graph Set Clustering

Source code in src/collaboration_detection/clustering/clustering.py
class Clustering:

    @staticmethod
    def execute(graph_collection: GraphCollection, algorithm: Algorithm, distance_metric: DistanceMetric,
                number_of_clusters: int | None = None,
                cluster_division_selector_metric: ClusterDivisionSelectorMetric | None = None,
                cluster_representative_seeder: ClusterRepresentativeSeeder | None = None,
                cluster_dissimilarity: float | None = None,
                cluster_centroid_selector: ClusterCentroidSelector | None = None) -> list[Cluster]:
        """
        This function is the entry point into the clustering package.

        :param graph_collection: the graph collection to get the graphs from.
            The resulting clusters are updated inplace.
        :param algorithm: the clustering_tests algorithm to use for clustering_tests
        :param distance_metric: the distance metric to calculate the distances between the graphs
        :param number_of_clusters: the number of clusters as stopping criterion for the hierarchical algorithm
        :param cluster_division_selector_metric: the selector for the cluster to divided for the hierarchical algorithm
        :param cluster_representative_seeder: the seed selector for the split clusters of the hierarchical algorithm
        :param cluster_dissimilarity: the dissimilarity measure for the dissimilar cluster centroid selector for density
            and partitioning algorithm
        :param cluster_centroid_selector: the selected cluster centroid selector for the partitioning algorithm
        :return: Resulting clusters
        """
        if len(graph_collection) == 0:
            return []
        distance_metric_object: BinaryDistanceMeasure | None = None
        graphs: list[GraphMap] | list[nx.DiGraph] | None = None
        if distance_metric == DistanceMetric.GRAPH_MAP_SIMILARITY_RATIO:
            distance_metric_object = MapGraphSimilarityRatio()
            graphs = graph_collection.as_graph_maps()
        if distance_metric == DistanceMetric.GRAPH_EDIT_DISTANCE:
            distance_metric_object = NetworkXGraphEditDistance()
            graphs = graph_collection.as_networkx_digraphs()
        if distance_metric == DistanceMetric.GRAPH_MAP_EDIT_DISTANCE:
            distance_metric_object = MapGraphEditDistance()
            graphs = graph_collection.as_graph_maps()
        assert distance_metric_object is not None
        assert graphs is not None

        cluster_division_selector_metric_object: ClustersEvaluator | None = None
        if cluster_division_selector_metric == ClusterDivisionSelectorMetric.COMPLETE_LINKAGE:
            cluster_division_selector_metric_object = MostDispersedClustersEvaluator(CompleteLink())
        if cluster_division_selector_metric == ClusterDivisionSelectorMetric.AVERAGE_LINKAGE:
            cluster_division_selector_metric_object = MostDispersedClustersEvaluator(AverageLinkage())

        cluster_representative_selector_object: MostDistantObjectSelector | None = None
        if cluster_representative_seeder == ClusterRepresentativeSeeder.MOST_DISTANT_FROM_REPRESENTATIVE:
            cluster_representative_selector_object = MostDistantRepresentativeSelector()
        if cluster_representative_seeder == ClusterRepresentativeSeeder.MOST_DISTANT_TOTAL:
            cluster_representative_selector_object = MostDistantTotalSelector()

        cluster_centroid_selector_object: ClusterCentroidEvaluator | None = None
        if cluster_centroid_selector == ClusterCentroidSelector.DISTINCT_CLUSTER_CENTROID_SELECTOR:
            cluster_centroid_selector_object = SimpleDistinctClusterCentroidSelector()
        if cluster_centroid_selector == ClusterCentroidSelector.DISSIMILAR_CLUSTER_CENTROID_SELECTOR:
            cluster_centroid_selector_object = SimpleDissimilarClusterCentroidSelector(cluster_dissimilarity)

        algorithm_object: ClusterAlgorithm | None = None
        if algorithm == Algorithm.HIERARCHICAL:
            assert number_of_clusters is not None
            assert cluster_division_selector_metric_object is not None
            assert cluster_representative_selector_object is not None
            algorithm_object = Hierarchical(SingleClusterParser(),
                                            AdjacencyMatrixCreator(distance_metric_object),
                                            ClusterDivisionExecutor(
                                                NumberOfClusters(number_of_clusters),
                                                cluster_division_selector_metric_object,
                                                WholeNumberClusterDivider(cluster_representative_selector_object),
                                                ClusterRepresentativeSelector())
                                            )
        if algorithm == Algorithm.PARTITIONING:
            assert distance_metric_object is not None
            assert cluster_centroid_selector_object is not None
            algorithm_object = Partitioning(AdjacencyMatrixCreator(distance_metric_object),
                                            cluster_centroid_selector_object,
                                            ClusterParser(),
                                            ShortestDistancePopulator()
                                            )
        if algorithm == Algorithm.DENSITY_BASED:
            assert cluster_dissimilarity is not None
            algorithm_object = DensityBased(
                AdjacencyGraphCreator(MapGraphSimilarityRatio(), cluster_dissimilarity),
                ConnectedComponentsParser()
            )
        assert algorithm_object is not None
        clustering_result = algorithm_object.cluster(graphs)
        Clustering._update_g_c_with_clustering_results(graph_collection, clustering_result)
        return clustering_result

    @staticmethod
    def _update_g_c_with_clustering_results(graph_collection: GraphCollection, clustering_result: list[Cluster]):
        graph_collection.clusters = {}
        for cluster in clustering_result:
            # Create a graph set cluster object
            g_c_cluster = GraphSetCluster(
                cluster_id=cluster.index,
                attributes={
                    'cluster_name': '',
                    'cluster_description': '',
                },
                representative=graph_collection.graphs[int(cluster.representative.data.label)]
                if cluster.representative else None
            )
            # Update the sub-graphs with their cluster id and add them to the g_c cluster list
            graph_collection.clusters[cluster.index] = g_c_cluster
            for object in cluster.objects:
                g_c_graph = graph_collection.graphs[int(object.data.label)]
                g_c_graph.cluster_id = int(cluster.index)
                g_c_cluster.graphs.add(g_c_graph)

execute(graph_collection, algorithm, distance_metric, number_of_clusters=None, cluster_division_selector_metric=None, cluster_representative_seeder=None, cluster_dissimilarity=None, cluster_centroid_selector=None) staticmethod

This function is the entry point into the clustering package.

:param graph_collection: the graph collection to get the graphs from. The resulting clusters are updated inplace. :param algorithm: the clustering_tests algorithm to use for clustering_tests :param distance_metric: the distance metric to calculate the distances between the graphs :param number_of_clusters: the number of clusters as stopping criterion for the hierarchical algorithm :param cluster_division_selector_metric: the selector for the cluster to divided for the hierarchical algorithm :param cluster_representative_seeder: the seed selector for the split clusters of the hierarchical algorithm :param cluster_dissimilarity: the dissimilarity measure for the dissimilar cluster centroid selector for density and partitioning algorithm :param cluster_centroid_selector: the selected cluster centroid selector for the partitioning algorithm :return: Resulting clusters

Source code in src/collaboration_detection/clustering/clustering.py
@staticmethod
def execute(graph_collection: GraphCollection, algorithm: Algorithm, distance_metric: DistanceMetric,
            number_of_clusters: int | None = None,
            cluster_division_selector_metric: ClusterDivisionSelectorMetric | None = None,
            cluster_representative_seeder: ClusterRepresentativeSeeder | None = None,
            cluster_dissimilarity: float | None = None,
            cluster_centroid_selector: ClusterCentroidSelector | None = None) -> list[Cluster]:
    """
    This function is the entry point into the clustering package.

    :param graph_collection: the graph collection to get the graphs from.
        The resulting clusters are updated inplace.
    :param algorithm: the clustering_tests algorithm to use for clustering_tests
    :param distance_metric: the distance metric to calculate the distances between the graphs
    :param number_of_clusters: the number of clusters as stopping criterion for the hierarchical algorithm
    :param cluster_division_selector_metric: the selector for the cluster to divided for the hierarchical algorithm
    :param cluster_representative_seeder: the seed selector for the split clusters of the hierarchical algorithm
    :param cluster_dissimilarity: the dissimilarity measure for the dissimilar cluster centroid selector for density
        and partitioning algorithm
    :param cluster_centroid_selector: the selected cluster centroid selector for the partitioning algorithm
    :return: Resulting clusters
    """
    if len(graph_collection) == 0:
        return []
    distance_metric_object: BinaryDistanceMeasure | None = None
    graphs: list[GraphMap] | list[nx.DiGraph] | None = None
    if distance_metric == DistanceMetric.GRAPH_MAP_SIMILARITY_RATIO:
        distance_metric_object = MapGraphSimilarityRatio()
        graphs = graph_collection.as_graph_maps()
    if distance_metric == DistanceMetric.GRAPH_EDIT_DISTANCE:
        distance_metric_object = NetworkXGraphEditDistance()
        graphs = graph_collection.as_networkx_digraphs()
    if distance_metric == DistanceMetric.GRAPH_MAP_EDIT_DISTANCE:
        distance_metric_object = MapGraphEditDistance()
        graphs = graph_collection.as_graph_maps()
    assert distance_metric_object is not None
    assert graphs is not None

    cluster_division_selector_metric_object: ClustersEvaluator | None = None
    if cluster_division_selector_metric == ClusterDivisionSelectorMetric.COMPLETE_LINKAGE:
        cluster_division_selector_metric_object = MostDispersedClustersEvaluator(CompleteLink())
    if cluster_division_selector_metric == ClusterDivisionSelectorMetric.AVERAGE_LINKAGE:
        cluster_division_selector_metric_object = MostDispersedClustersEvaluator(AverageLinkage())

    cluster_representative_selector_object: MostDistantObjectSelector | None = None
    if cluster_representative_seeder == ClusterRepresentativeSeeder.MOST_DISTANT_FROM_REPRESENTATIVE:
        cluster_representative_selector_object = MostDistantRepresentativeSelector()
    if cluster_representative_seeder == ClusterRepresentativeSeeder.MOST_DISTANT_TOTAL:
        cluster_representative_selector_object = MostDistantTotalSelector()

    cluster_centroid_selector_object: ClusterCentroidEvaluator | None = None
    if cluster_centroid_selector == ClusterCentroidSelector.DISTINCT_CLUSTER_CENTROID_SELECTOR:
        cluster_centroid_selector_object = SimpleDistinctClusterCentroidSelector()
    if cluster_centroid_selector == ClusterCentroidSelector.DISSIMILAR_CLUSTER_CENTROID_SELECTOR:
        cluster_centroid_selector_object = SimpleDissimilarClusterCentroidSelector(cluster_dissimilarity)

    algorithm_object: ClusterAlgorithm | None = None
    if algorithm == Algorithm.HIERARCHICAL:
        assert number_of_clusters is not None
        assert cluster_division_selector_metric_object is not None
        assert cluster_representative_selector_object is not None
        algorithm_object = Hierarchical(SingleClusterParser(),
                                        AdjacencyMatrixCreator(distance_metric_object),
                                        ClusterDivisionExecutor(
                                            NumberOfClusters(number_of_clusters),
                                            cluster_division_selector_metric_object,
                                            WholeNumberClusterDivider(cluster_representative_selector_object),
                                            ClusterRepresentativeSelector())
                                        )
    if algorithm == Algorithm.PARTITIONING:
        assert distance_metric_object is not None
        assert cluster_centroid_selector_object is not None
        algorithm_object = Partitioning(AdjacencyMatrixCreator(distance_metric_object),
                                        cluster_centroid_selector_object,
                                        ClusterParser(),
                                        ShortestDistancePopulator()
                                        )
    if algorithm == Algorithm.DENSITY_BASED:
        assert cluster_dissimilarity is not None
        algorithm_object = DensityBased(
            AdjacencyGraphCreator(MapGraphSimilarityRatio(), cluster_dissimilarity),
            ConnectedComponentsParser()
        )
    assert algorithm_object is not None
    clustering_result = algorithm_object.cluster(graphs)
    Clustering._update_g_c_with_clustering_results(graph_collection, clustering_result)
    return clustering_result