smallgraphlib 0.6.2__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: smallgraphlib
3
- Version: 0.6.2
3
+ Version: 0.7.1
4
4
  Summary: Simple library for handling small graphs, including Tikz code generation.
5
5
  Home-page: https://github.com/wxgeo/smallgraphlib
6
6
  License: GPL-3.0-or-later
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
15
16
  Project-URL: Repository, https://github.com/wxgeo/smallgraphlib
16
17
  Description-Content-Type: text/markdown
17
18
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "smallgraphlib"
3
- version = "0.6.2"
3
+ version = "0.7.1"
4
4
  description = "Simple library for handling small graphs, including Tikz code generation."
5
5
  authors = ["Nicolas Pourcelot <nicolas.pourcelot@gmail.com>"]
6
6
  repository = "https://github.com/wxgeo/smallgraphlib"
@@ -11,7 +11,7 @@ keywords = ["graph", "tikz", "latex"]
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.10"
13
13
 
14
- [tool.poetry.dev-dependencies]
14
+ [tool.poetry.group.dev.dependencies]
15
15
  pytest = "^7"
16
16
  mypy = "^1.0"
17
17
  flake8 = "^6"
@@ -22,13 +22,19 @@ sphinx-rtd-theme = "^1.0.0"#
22
22
  myst-parser = "^0.18.0"
23
23
  # To test compatibility with sympy.
24
24
  sympy = "^1.10.1"
25
+ # Version 7.29+ are buggy !
26
+ python-semantic-release = "7.28.1"
25
27
 
26
28
  [build-system]
27
29
  requires = ["poetry-core>=1.0.0"]
28
30
  build-backend = "poetry.core.masonry.api"
29
31
 
32
+ [tool.semantic_release]
33
+ version_variable = "pyproject.toml:version"
34
+
30
35
  [tool.mypy]
31
36
  implicit_optional = true
37
+ warn_unused_ignores = true
32
38
 
33
39
  [tool.black]
34
40
  line-length = 110
@@ -46,5 +52,5 @@ commands =
46
52
  poetry run black smallgraphlib tests
47
53
  poetry run pytest tests
48
54
  poetry run mypy smallgraphlib tests
49
- poetry run flake8 --max-line-length 110 --ignore=E203,W503,W391
55
+ poetry run flake8 --max-line-length 110 --ignore=E203,W503,W391 --per-file-ignores="tests/test_latex_export.py:E501"
50
56
  """
@@ -231,6 +231,8 @@ class AbstractGraph(ABC, Generic[Node]):
231
231
  >>> g.is_isomorphic_to(g2)
232
232
  True
233
233
  """
234
+ # Sort nodes before shuffling, to make shuffling deterministic with a given random.seed.
235
+ # nodes = sorted(self.nodes)
234
236
  nodes = list(self.nodes)
235
237
  random.shuffle(nodes)
236
238
  self.rename_nodes(dict((old_name, new_name) for old_name, new_name in zip(self.nodes, nodes)))
@@ -0,0 +1,150 @@
1
+ from collections import Counter
2
+ from operator import attrgetter
3
+
4
+ from smallgraphlib.custom_types import Node
5
+
6
+
7
+ class Tree:
8
+ branches: tuple["Tree", ...]
9
+
10
+ def __init__(self, root: Node, *branches: "Tree") -> None:
11
+ self.root = root
12
+ self.branches = branches
13
+
14
+
15
+ class HuffmanTree(Tree):
16
+ branches: tuple["HuffmanTree", ...]
17
+
18
+ def __init__(self, *branches: "HuffmanTree", char: str = None, weight: int = None) -> None:
19
+ if len(branches) == 2:
20
+ if char is not None or weight is not None:
21
+ raise ValueError("Char and weight can't be set, except for leaves.")
22
+ elif len(branches) == 0:
23
+ if char is None or weight is None:
24
+ raise ValueError("Char and weight must be set for leaves.")
25
+ else:
26
+ raise ValueError(f"There must be either 0 or 2 branches, not {len(branches)}.")
27
+ char = min(branch.char for branch in branches) if branches else char
28
+ weight = sum(branch.weight for branch in branches) if branches else weight
29
+ super().__init__((weight, char), *branches)
30
+
31
+ @classmethod
32
+ def from_text(cls, text: str) -> "HuffmanTree":
33
+ trees = {
34
+ HuffmanTree(char=letter, weight=occurrences) for letter, occurrences in Counter(text).items()
35
+ }
36
+ sort_func = attrgetter("root")
37
+ while len(trees) >= 2:
38
+ # Select the two smallest values
39
+ tree1 = min(trees, key=sort_func)
40
+ trees.remove(tree1)
41
+ tree2 = min(trees, key=sort_func)
42
+ trees.remove(tree2)
43
+ trees.add(HuffmanTree(tree1, tree2))
44
+ assert len(trees) == 1
45
+ return trees.pop()
46
+
47
+ @property
48
+ def weight(self):
49
+ return self.root[0]
50
+
51
+ @property
52
+ def char(self):
53
+ return self.root[1]
54
+
55
+ @property
56
+ def is_leaf(self) -> bool:
57
+ return len(self.branches) == 0
58
+
59
+ @property
60
+ def left_branch(self) -> "HuffmanTree":
61
+ return self.branches[0]
62
+
63
+ @property
64
+ def right_branch(self) -> "HuffmanTree":
65
+ return self.branches[1]
66
+
67
+ def compress(self, text: str) -> bytes:
68
+ compressed = []
69
+ buffer = ""
70
+ for char in text:
71
+ buffer += self.labels[char]
72
+ while len(buffer) >= 8:
73
+ compressed.append(sum(2**i * int(c) for i, c in enumerate(buffer[:8])))
74
+ buffer = buffer[8:]
75
+ # On finit de vider le buffer.
76
+ # Cela revient à ajouter des bits nuls, puisque le nombre de bits final doit
77
+ # être un multiple de 8 (on ne peut renvoyer que des octets complets).
78
+ # Si on voulait créer un logiciel de compression réellement utilisable, il faudrait donc
79
+ # soit avoir un caractère de fin de chaîne, soit préciser le nombre de bits finaux
80
+ # inutiles (entre 0 et 7).
81
+ # On pourrait par exemple décider que par convention les 3 premiers bits servent à encoder le
82
+ # nombre de bits finaux inutiles.
83
+ # Accessoirement, il faudrait aussi que la chaîne de caractères intègre le dictionnaire de compression
84
+ # en début de chaîne, et donc se mettre d'accord sur un format, etc.
85
+ if buffer:
86
+ compressed.append(sum(2**i * int(c) for i, c in enumerate(buffer[:8])))
87
+ return bytes(compressed)
88
+
89
+ def uncompress(self, bits: bytes) -> str:
90
+ position = self
91
+ read: list[str] = []
92
+ for byte in bits:
93
+ for i in range(8):
94
+ digit, byte = byte % 2, byte // 2
95
+ position = position.branches[digit]
96
+ if position.is_leaf:
97
+ read.append(position.char)
98
+ position = self
99
+ return "".join(read)
100
+
101
+ def decode(self, bits: str) -> str:
102
+ position = self
103
+ read: list[str] = []
104
+ for digit in bits:
105
+ position = position.branches[int(digit)]
106
+ if position.is_leaf:
107
+ read.append(position.char)
108
+ position = self
109
+ return "".join(read)
110
+
111
+ def encode(self, text: str) -> str:
112
+ return "".join(self.labels[char] for char in text)
113
+
114
+ @property
115
+ def labels(self) -> dict[str, str]:
116
+ """Binary labels."""
117
+ if self.is_leaf:
118
+ return {self.char: ""}
119
+ return {
120
+ key: str(i) + value
121
+ for i, branch in enumerate(self.branches)
122
+ for key, value in branch.labels.items()
123
+ }
124
+
125
+ def __repr__(self):
126
+ if self.is_leaf:
127
+ return f"HuffmanTree(char={self.char}, weight={self.weight})"
128
+ return f"HuffmanTree({self.left_branch!r}, {self.right_branch!r})"
129
+
130
+ def __str__(self):
131
+ lines = []
132
+ shift = len(str(self.weight)) + 1
133
+ for n, line in enumerate(str(self.left_branch).split("\n")):
134
+ if n == 0:
135
+ # lines.append("\u252C\u2500\u2500" + line)
136
+ lines.append(f"{self.weight}\u2500\u2500" + line)
137
+ else:
138
+ lines.append("\u2502" + shift * " " + line)
139
+ for n, line in enumerate(str(self.right_branch).split("\n")):
140
+ if n == 0:
141
+ lines.append("\u2514" + shift * "\u2500" + line)
142
+ else:
143
+ lines.append((shift + 1) * " " + line)
144
+ return "\n".join(lines)
145
+
146
+
147
+ def encode(text: str) -> str:
148
+ """Return a string of `0` and `1` representing the bits of a text encoded using Huffman algorithm."""
149
+ tree = HuffmanTree.from_text(text)
150
+ return tree.encode(text)
@@ -0,0 +1,108 @@
1
+ import math
2
+ from typing import Iterable
3
+
4
+ from smallgraphlib.core import AbstractGraph
5
+ from smallgraphlib.custom_types import Node
6
+
7
+
8
+ def latex_Dijkstra(graph: AbstractGraph[Node], start: Node, end: Node = None) -> str:
9
+ """Generate the LaTeX code of a table corresponding to Dijkstra algorithm's steps.
10
+
11
+ If `end` is `None`, only stop when the shorter path to all other nodes have been found.
12
+ """
13
+ nodes = graph.nodes
14
+ num_cols = len(nodes) + 2
15
+ lines: list[str] = [
16
+ rf"\begin{{tabular}}{{|*{num_cols}{{c |}}}}\cline{{2-{num_cols}}}",
17
+ r"\multicolumn{1}{c|}{} & "
18
+ + " & ".join(sorted(f"${node}$" for node in nodes))
19
+ + r" & Selected\\\hline",
20
+ ]
21
+ # Nodes which have been already visited, but still not archived:
22
+ # visited: dict[Node, tuple[float, list[Node]]] = {start: (0, [start])}
23
+ # format: {node: [distance from start, [previous node, alternative previous node, ...]]}
24
+
25
+ previous_nodes: dict[Node, set[Node]] = {node: set() for node in graph.nodes}
26
+ distance_from_start: dict[Node, float] = {node: math.inf for node in graph.nodes}
27
+ being_processed: set[Node] = {start}
28
+ completed: set[Node] = set()
29
+ distance_from_start[start] = 0
30
+
31
+ # Nodes which will not change anymore (shorter path from start has been found):
32
+ # archived: dict[Node, tuple[float, list[Node]]] = {}
33
+
34
+ def cell_content(node: Node) -> str:
35
+ """Used to print the node in the table."""
36
+ dist = distance_from_start[node]
37
+ previous = previous_nodes[node]
38
+ if dist == math.inf:
39
+ return r"$+\infty$"
40
+ if node in completed:
41
+ return r"\cellcolor{lightgray}"
42
+ printing = str(dist)
43
+ if node != start:
44
+ printing += f" $({','.join(sorted(str(node) for node in previous))})$"
45
+ if node == current:
46
+ printing = rf"\cellcolor{{blue!20}}\textbf{{{printing}}}"
47
+ return printing
48
+
49
+ current: Node
50
+ first_cell = r"\text{start}"
51
+ while being_processed and end != (
52
+ current := min(being_processed, key=(lambda n: distance_from_start[n]))
53
+ ):
54
+ lines.append(
55
+ f"${first_cell}$ & "
56
+ + " & ".join(cell_content(node) for node in nodes)
57
+ + rf" & {current} {cell_content(current)}\\\hline"
58
+ )
59
+ first_cell = str(current)
60
+ being_processed.remove(current)
61
+ completed.add(current)
62
+ # We update the distances
63
+ for neighbor in graph.successors(current):
64
+ if neighbor not in completed:
65
+ being_processed.add(neighbor)
66
+ # best distance found until now between neighbor and start
67
+ current_distance = distance_from_start[neighbor]
68
+ # new distance found using current node:
69
+ new_distance = distance_from_start[current] + graph.weight(current, neighbor)
70
+ # replace with new distance only if better
71
+ if new_distance < current_distance:
72
+ distance_from_start[neighbor] = new_distance
73
+ previous_nodes[neighbor] = {current}
74
+ elif new_distance == current_distance:
75
+ previous_nodes[neighbor].add(current)
76
+
77
+ lines.append("\\end{tabular}")
78
+ lines.append("")
79
+
80
+ def shortest_paths() -> str:
81
+ paths_in_construction = [[target]]
82
+ completed_paths = []
83
+ while paths_in_construction:
84
+ partial_path = paths_in_construction.pop()
85
+ assert len(partial_path) > 0
86
+ for previous in previous_nodes[partial_path[0]]:
87
+ if previous == start:
88
+ completed_paths.append([previous] + partial_path)
89
+ else:
90
+ paths_in_construction.append([previous] + partial_path)
91
+
92
+ def path_to_str(path: Iterable[Node]):
93
+ return "-".join(str(node) for node in path)
94
+
95
+ return ",".join(path_to_str(path) for path in completed_paths)
96
+
97
+ targets = list(nodes) if end is None else [end]
98
+
99
+ for target in targets:
100
+ if target != start:
101
+ distance = distance_from_start[target]
102
+ lines.append(
103
+ f"Shorter(s) path(s) from ${start}$ to ${target}$: "
104
+ f"${shortest_paths()}$ (length: {distance})."
105
+ )
106
+ lines.append("")
107
+
108
+ return "\n" + "\n".join(lines)
File without changes
@@ -212,7 +212,7 @@ class TikzPrinter(Generic[Node]):
212
212
  r"\usepackage{tikz}",
213
213
  r"\usetikzlibrary{arrows.meta}",
214
214
  r"\usepackage[outline]{contour}",
215
- r"\contourlength{0.5pt}",
215
+ r"\contourlength{0.15em}",
216
216
  ]
217
217
 
218
218
  def tikz_code(self, graph, *, shuffle_nodes=False, options="") -> str:
@@ -226,7 +226,7 @@ class TikzPrinter(Generic[Node]):
226
226
  For labeled graphs, it is recommended to load `contour` package too::
227
227
 
228
228
  \usepackage[outline]{contour}
229
- \contourlength{0.5pt}
229
+ \contourlength{0.15em}
230
230
 
231
231
  """
232
232
  self.graph = graph
@@ -1,26 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['smallgraphlib']
6
-
7
- package_data = \
8
- {'': ['*']}
9
-
10
- setup_kwargs = {
11
- 'name': 'smallgraphlib',
12
- 'version': '0.6.2',
13
- 'description': 'Simple library for handling small graphs, including Tikz code generation.',
14
- 'long_description': '# Small Graph Lib\n\n## Installing\n\n $ git clone https://github.com/wxgeo/smallgraphlib\n\n $ pip install --user smallgraphlib\n\n## Usage\n\nMain classes are `Graph`, `DirectedGraph`, `WeightedGraph` and `WeightedDirectedGraph`:\n\n >>> from smallgraphlib import DirectedGraph\n >>> g = DirectedGraph(["A", "B", "C"], ("A", "B"), ("B", "A"), ("B", "C"))\n >>> g.is_simple\n True\n >>> g.is_complete\n False\n >>> g.is_directed\n True\n >>> g.adjacency_matrix\n [[0, 1, 0], [1, 0, 1], [0, 0, 0]]\n >>> g.degree\n 3\n >>> g.order\n 3\n >>> g.is_eulerian\n False\n >>> g.is_semi_eulerian\n True\n\nSpecial graphs may be generated using factory functions:\n \n >>> from smallgraphlib import complete_graph, complete_bipartite_graph\n >>> K5 = complete_graph(5)\n >>> len(K5.greedy_coloring)\n 5\n >>> K33 = complete_bipartite_graph(3, 3)\n >>> K33.degree\n 6\n >>> K33.diameter\n 2\n \nIf the graph is not too complex, Tikz code may be generated:\n\n >>> g.as_tikz()\n ...\n\n## Development\n\n1. Get last version:\n \n $ git clone https://github.com/wxgeo/smallgraphlib\n\n2. Install Poetry.\n \n Poetry is a tool for dependency management and packaging in Python.\n\n Installation instructions are here:\n https://python-poetry.org/docs/#installation\n\n3. Install developments tools:\n \n $ poetry install\n\n4. Optionally, update development tools:\n \n $ poetry update\n\n5. Optionally, install library in editable mode:\n\n $ pip install -e smallgraphlib\n\n6. Make changes, add tests.\n \n7. Launch tests:\n\n $ tox\n\n8. Everything\'s OK ? Commit. :)',
15
- 'author': 'Nicolas Pourcelot',
16
- 'author_email': 'nicolas.pourcelot@gmail.com',
17
- 'maintainer': 'None',
18
- 'maintainer_email': 'None',
19
- 'url': 'https://github.com/wxgeo/smallgraphlib',
20
- 'packages': packages,
21
- 'package_data': package_data,
22
- 'python_requires': '>=3.10,<4.0',
23
- }
24
-
25
-
26
- setup(**setup_kwargs)
File without changes