Skip to content

Commit eff1251

Browse files
authored
Release v1.1.0
* Change package management to poetry * Add more type annotations for better static code checking * Add pre-commit configuration to ensure code quality * Optimize the algorithm implementation
1 parent 289657d commit eff1251

21 files changed

Lines changed: 1210 additions & 1347 deletions

.pre-commit-config.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: check-json
6+
- id: check-toml
7+
- id: check-xml
8+
- id: check-yaml
9+
- id: detect-private-key
10+
- repo: https://github.com/psf/black
11+
rev: 23.9.1
12+
hooks:
13+
- id: black
14+
- repo: https://github.com/PyCQA/docformatter
15+
rev: v1.7.5
16+
hooks:
17+
- id: docformatter
18+
additional_dependencies: [tomli]
19+
- repo: https://github.com/astral-sh/ruff-pre-commit
20+
rev: v0.0.292
21+
hooks:
22+
- id: ruff
23+
args: [--fix, --exit-non-zero-on-fix]
24+
- repo: https://github.com/pre-commit/mirrors-mypy
25+
rev: v1.6.0
26+
hooks:
27+
- id: mypy
28+
additional_dependencies: [types-PyYAML]

LICENSE

Lines changed: 0 additions & 19 deletions
This file was deleted.

MANIFEST.in

Lines changed: 0 additions & 3 deletions
This file was deleted.

README.md

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,122 @@
1-
## Topological Sorting Algorithm for Cyclic Graphs ##
1+
## Topological Sorting Algorithm for Cyclic Graphs
22

3-
**Version 1.0.0**
3+
**Version 1.1.0**
44

5-
![Python version req](https://img.shields.io/badge/python-v3.0%2B-informational)
5+
![Python version req](https://img.shields.io/badge/python-v3.10%2B-informational)
66
[![PyPI version](https://badge.fury.io/py/cyclic-toposort.svg)](https://badge.fury.io/py/cyclic-toposort)
77
[![codecov](https://codecov.io/gh/PaulPauls/cyclic-toposort/branch/master/graph/badge.svg)](https://codecov.io/gh/PaulPauls/cyclic-toposort)
88

9-
Sorting algorithm for cyclic as well as acyclic directed graphs such as those below. A directed graph is cyclic if any node exists that has a directed path leading to another node and back to the origin node.
9+
Sorting algorithm for cyclic as well as acyclic directed graphs such as those below. A directed graph is cyclic if any node exists within the graph that has a directed path leading to at least 1 other node and then back to the original node again.
1010

1111
<p align="center">
12-
<img src="./illustrations/cyclic_toposort_graphs.svg" width="60%" alt="Example cyclic and acyclic graphs"/>
12+
<img src="./.illustrations/cyclic_toposort_graphs.svg" width="60%" alt="Example cyclic and acyclic graphs"/>
1313
</p>
1414

15-
The project provides three sorting algorithms for these graphs. `cyclic_topoosort` sorts a cyclic graph and returns a 2-tuple with the first element being a list of ordered nodes and the second element being a set of 2-tuples that are the cyclic edges. The set of cyclic edges is minimal and if the graph is acyclic will be an empty set. `cyclic_toposort_groupings` functions identical though will return as the first element of the 2-tuple an ordered list of sets of nodes, representing topological levels that can be visited at the same time. The set of cyclic edges is also minimal with the groupings variant and empty if the graph is acyclic. `acyclic_toposort` sorts only acyclic graphs and returns an ordered list of sets of nodes, again representing the topological levels.
15+
This project provides 2 sorting algorithms for these graphs. A graph is represented as a set of edges with an edge being a 2-tuple describing the start-node and the end-node of an edge.
16+
17+
`acyclic_toposort` sorts an acyclic graph (or raises a RuntimeError if called on a cyclic graph) into a list of topological groupings. These topological groupings are sets of nodes that are on the same topological level. Nodes in a topological grouping are either dependent on an incoming edge from the prior topological grouping or have no incoming edges if they are in the first topological grouping.
18+
19+
```python3
20+
def acyclic_toposort(edges: Iterable[tuple[int, int]]) -> list[set[int]]:
21+
"""Create and return a topological sorting of an acyclic graph as a list of sets, each set representing a
22+
topological level, starting with the nodes that have no dependencies.
23+
24+
:param edges: iterable of edges represented as 2-tuples, whereas each 2-tuple represents the start-index and end-
25+
index of an edge
26+
:return: topological sorting of the graph represented by the input edges as a list of sets that represent each
27+
topological level in order beginning with all dependencyless nodes.
28+
:raises RuntimeError: if a cyclic graph is detected.
29+
"""
30+
```
31+
32+
`cyclic_topoosort` on the other hand sorts cyclic graphs and returns a 2-tuple with the first element being the same list of topological groupings that is returned in the `acyclic_toposort` function and the second element being a set of edges that is required to be cyclic in order to make the rest of the graph acyclic. The determined set of cyclic edges is minimal and if the graph is acyclic will be an empty set. If there are multiple sets of cyclic edges that would turn the rest of the graph acyclic and all have the same size then the set of cyclic edges is chosen which enables the acyclic restgraph to be sorted with the least amount of topological groupings. Unfortunately does this algorithm employ full polynomial recursion and can have a runtime of up to O(2^n).
33+
34+
```python3
35+
def cyclic_toposort(
36+
edges: set[tuple[int, int]],
37+
start_node: int | None = None,
38+
) -> tuple[list[set[int]], set[tuple[int, int]]]:
39+
"""Perform a topological sorting on a potentially cyclic graph, returning a tuple consisting of a graph topology
40+
with the fewest topological groupings and a minimal set of cyclic edges.
41+
42+
:param edges: A set of tuples where each tuple represents a directed edge (start_node, end_node) in the graph.
43+
:param start_node: An optional node. If provided, any edge leading into this node will be considered as a forced
44+
cyclic edge.
45+
:return: A tuple containing:
46+
- A list of sets representing the topological ordering of nodes. Each set contains nodes at the same depth. The
47+
amount of topological groupings is minimal out of all possible sets of cyclic edges.
48+
- A set of tuples representing the cyclic edges that were identified in the graph and that yielded a graph
49+
topology with the fewest topological groupings.
50+
"""
51+
```
1652

1753

1854
------------------------------------------------------------------------------------------------------------------------
1955

20-
### Example Usage ###
56+
### Installation & Example Usage
2157

22-
The following examples encode the cyclic and acyclic graphs displayed above:
58+
Install `cyclic-toposort` via pip or your preferred Python package manager:
59+
60+
```shell
61+
pip install cyclic-toposort
62+
```
63+
64+
The following examples encode the cyclic and acyclic graphs displayed above and show the usage of cyclic-toposort as a package:
2365

2466
``` python
25-
>>> edges = {(1, 2), (2, 3), (3, 5), (3, 6), (4, 1), (4, 5), (4, 6), (5, 2), (5, 7), (6, 1), (8, 6)}
26-
>>> cyclic_toposort(edges)
27-
([8, 3, 4, 5, 6, 1, 7, 2], {(2, 3)})
28-
>>> cyclic_toposort_groupings(edges)
29-
([{8, 3, 4}, {5, 6}, {1, 7}, {2}], {(2, 3)})
30-
>>> cyclic_toposort_groupings(edges, start_node=2, end_node=5)
31-
([{8, 2, 4}, {3}, {6}, {1, 5}], {(1, 2), (5, 7), (5, 2)})
67+
>>> from cyclic_toposort import cyclic_toposort, acyclic_toposort
3268

69+
>>> cyclic_graph_edges = {(1, 2), (2, 3), (3, 5), (3, 6), (4, 1), (4, 5), (4, 6), (5, 2), (5, 7), (6, 1), (8, 6)}
70+
>>> cyclic_toposort(cyclic_graph_edges)
71+
([{8, 3, 4}, {5, 6}, {1, 7}, {2}], {(2, 3)})
72+
>>> cyclic_toposort(cyclic_graph_edges, start_node=2)
73+
([{8, 2, 4}, {3}, {5, 6}, {1, 7}], {(1, 2), (5, 2)})
3374

34-
>>> edges = {(1, 2), (1, 3), (2, 3), (2, 4), (3, 4), (5, 3), (5, 6), (7, 6)}
35-
>>> acyclic_toposort(edges)
75+
>>> acyclic_toposort_edges = {(1, 2), (1, 3), (2, 3), (2, 4), (3, 4), (5, 3), (5, 6), (7, 6)}
76+
>>> acyclic_toposort(acyclic_toposort_edges)
3677
[{1, 5, 7}, {2, 6}, {3}, {4}]
3778
```
3879

3980

4081
------------------------------------------------------------------------------------------------------------------------
4182

42-
### Correctness and Performance ###
83+
### Correctness and Performance
4384

4485
Since I am unable to formerly validate the specifications of my algorithms have I opted to prove the correctness of the cyclic sorting algorithm by randomly generating cyclic graphs, sorting them with the algortihms and verifying the correctness of the results by testing them against a bruteforce sorting method that takes a long time though is able to calculate all correct results. The random graphs are generated with the following parameters:
4586

4687
``` python
88+
CYCLIC_NODES_PROBABILITY = 0.2
89+
START_NODE_PROBABILITY = 0.2
90+
4791
num_edges = random.randint(8, 16)
48-
start_node = random.choice([None, random.randint(1, 5)])
49-
end_node = random.choice([None, random.randint(6, 10)])
50-
full_cyclic_graph = False
51-
cyclic_nodes = random.choice([True, False])
52-
nodes, edges = test_utils.create_random_graph(num_edges=num_edges,
53-
start_node=start_node,
54-
end_node=end_node,
55-
full_cyclic_graph=full_cyclic_graph,
56-
cyclic_nodes=cyclic_nodes)
92+
cyclic_nodes = random.random() < CYCLIC_NODES_PROBABILITY
93+
edges = create_random_graph(
94+
num_edges=num_edges,
95+
cyclic_nodes=cyclic_nodes
96+
)
97+
start_node = None
98+
if random.random() < START_NODE_PROBABILITY:
99+
start_node = random.choice(list(edges))[0]
57100
```
58101

59-
This verification process is repeated 1000 times in the test files and yielded the following average processing times for the sorting algorithms given the graphs generated with the parameters above. The average processing times were calculated on a Ryzen 5 2600X (6 x 3.6Ghz):
60-
61-
`cyclic_toposort` mean. time: 0.4936s (std. dev: 2.6189s)
102+
This verification process is repeated 1000 times in the test files and yielded the following average processing times for the sorting algorithms given the graphs generated with the parameters above. The average processing times were calculated on a Ryzen 5 2600X (6 x 3.6Ghz) using Python 3.10.11:
62103

63-
`cyclic_toposort_groupings` mean. time: 0.8320s (std. dev: 4.3270s)
104+
`cyclic_toposort` mean. time: 0.2439s (std. dev: 2.658s)
64105

65106

66107
------------------------------------------------------------------------------------------------------------------------
67108

68-
### Dev Comments ###
109+
### Dev Comments
69110

70-
* The cyclic sorting algorithms are slow when applied to graphs that are fully cyclic (each node has at least 1 incoming and at least 1 outgoing edge). The Bruteforce method is surprisingly quick when the graph is fully cyclic.
111+
* The cyclic sorting algorithm is slow when applied to graphs that are fully cyclic (each node has at least 1 incoming and at least 1 outgoing edge). The Bruteforce method is surprisingly quick when the graph is fully cyclic.
71112

72-
* The implementaiton has further considerable speed up potential by using multithreading as it is currently single-threaded while being easily parallelizable. The algorithm would also benefit if implemented in a lower level programming language as it relies heavily on recursion and CPython is known to be ressource-hungry on recursion. If the project will be well received and gains some users then I will optimize the implementation (and possibly algorithm) more.
113+
* The implementation has considerable speed up potential by using multithreading, which however would require coordination of the multitude of recursively spawned threads. Since the algorithm relies heavily on recurstion and CPython is known to be slow and ressource-hungry on recursion have I not bothered in parallelizing the implementation for the maximum attainable speed since a proper implementation would require a low-level programming language like C++ or Rust anyway. If the project will be well received and gains some actual use then I hope to find the time to implement this project in Rust and make Python bindings available.
73114

74115
* I would be thankful for feedback, issues (with reproducing code) or even concrete ideas or code for improvement
75116

76117

77118
------------------------------------------------------------------------------------------------------------------------
78119

79-
### Known Issues ###
120+
### Known Issues
80121

81122
None

0 commit comments

Comments
 (0)