mplusa 0.0.2__tar.gz → 0.0.4__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.
mplusa-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: mplusa
3
+ Version: 0.0.4
4
+ Summary: A library for calculations in tropical and arctic semirings.
5
+ Author-email: Maksymilian Wiekiera <maksymilian3563@gmail.com>
6
+ License: Copyright (c) 2025 Maksymilian Wiekiera
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ Project-URL: homepage, https://github.com/Hadelekw/mplusa
26
+ Project-URL: documentation, https://maksymilian-wiekiera.fyi/mplusa/0.0.4.html
27
+ Classifier: Programming Language :: Python :: 3
28
+ Classifier: Operating System :: OS Independent
29
+ Requires-Python: >=3.11
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: numpy>=2.2.3
33
+ Dynamic: license-file
34
+
35
+ # MPlusA
36
+ **MPlusA** is a Python library for calculations in tropical algebra (also known as (min, +) and (max, +) algebra). For the full list of the library's capabilities refer to the [documentation](http://maksymilian-wiekiera.fyi/mplusa/0_0_4.html).
37
+
38
+ # Contributing
39
+ The library is open for all contributions. If you want to contribute to its development, please refer to the guidelines to verify the code standards before making a pull request. Developments from all areas of tropical mathematics are welcome.
40
+
41
+ # Installation
42
+ The easiest way to install the library is to use pip.
43
+
44
+ ``` pip install mplusa ```
45
+
46
+ The only automatically installed dependency is [NumPy](https://numpy.org). Otherwise the library does make use of [Matplotlib](https://matplotlib.org/) for visualisations but it is not a required dependency and needs to be installed separately in case the user wants to use these capabilities.
mplusa-0.0.4/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # MPlusA
2
+ **MPlusA** is a Python library for calculations in tropical algebra (also known as (min, +) and (max, +) algebra). For the full list of the library's capabilities refer to the [documentation](http://maksymilian-wiekiera.fyi/mplusa/0_0_4.html).
3
+
4
+ # Contributing
5
+ The library is open for all contributions. If you want to contribute to its development, please refer to the guidelines to verify the code standards before making a pull request. Developments from all areas of tropical mathematics are welcome.
6
+
7
+ # Installation
8
+ The easiest way to install the library is to use pip.
9
+
10
+ ``` pip install mplusa ```
11
+
12
+ The only automatically installed dependency is [NumPy](https://numpy.org). Otherwise the library does make use of [Matplotlib](https://matplotlib.org/) for visualisations but it is not a required dependency and needs to be installed separately in case the user wants to use these capabilities.
@@ -1,8 +1,8 @@
1
1
  [project]
2
2
  name = "mplusa"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  authors = [
5
- {name="Maksymilian W.", email="maksymilian3563@gmail.com"}
5
+ {name="Maksymilian Wiekiera", email="maksymilian3563@gmail.com"}
6
6
  ]
7
7
  description = "A library for calculations in tropical and arctic semirings."
8
8
  readme = "README.md"
@@ -18,3 +18,4 @@ license = {file="LICENSE"}
18
18
 
19
19
  [project.urls]
20
20
  homepage = "https://github.com/Hadelekw/mplusa"
21
+ documentation = "https://maksymilian-wiekiera.fyi/mplusa/0.0.4.html"
@@ -0,0 +1,4 @@
1
+ from .maxplus import *
2
+ from .domain import *
3
+ from . import geometry
4
+ from . import visualize
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+
3
+ import math
4
+ from typing import Any
5
+ from collections.abc import Collection
6
+
7
+
8
+ class ArcticNumberMeta(type):
9
+ def __instancecheck__(self, instance: Any, /) -> bool:
10
+ if isinstance(instance, float|int|np.floating|np.integer):
11
+ if instance < math.inf:
12
+ return True
13
+ return False
14
+
15
+
16
+ class ArcticNumber(metaclass=ArcticNumberMeta):
17
+ pass
18
+
19
+
20
+ def validate_domain(value : Any) -> None:
21
+ if isinstance(value, Collection):
22
+ for item in value:
23
+ validate_domain(item)
24
+ return
25
+ if isinstance(value, ArcticNumber):
26
+ return
27
+ raise ValueError('Value out of domain.')
@@ -0,0 +1,214 @@
1
+ import numpy as np
2
+
3
+ import math
4
+ from collections.abc import Collection
5
+
6
+ from .domain import validate_domain
7
+ from .maxplus import add, mult, add_matrices
8
+
9
+
10
+ def project_point(point : tuple) -> tuple:
11
+ validate_domain(point)
12
+ return tuple((point[i] - point[0]) for i in range(1, len(point)))
13
+
14
+
15
+ def point_type(point : Collection, vertices : Collection, indexing_start : int = 0) -> Collection:
16
+ result = [[] for _ in range(len(vertices))]
17
+ point = np.array(point)
18
+ vertices = np.array(vertices)
19
+ for i, vertex in enumerate(vertices):
20
+ comparison = add(*(vertex - point))
21
+ for j in range(len(vertex)):
22
+ if vertex[j] - point[j] == comparison:
23
+ result[j].append(indexing_start + i)
24
+ return result
25
+
26
+
27
+ def tconv(points : Collection[Collection]) -> list:
28
+ result = []
29
+ for point in points:
30
+ T = point_type(point, list(filter(lambda v: v != point, points)))
31
+ if [] in T:
32
+ result.append(point)
33
+ return result
34
+
35
+
36
+ def line_segment(start_point : Collection, end_point : Collection, sort=True, unique=True) -> np.ndarray:
37
+ x = np.array(start_point)
38
+ y = np.array(end_point)
39
+ result = []
40
+ for i in range(len(x)):
41
+ if y[i] <= x[i]:
42
+ result.append(add_matrices((y[i] - x[i]) + x, y))
43
+ else:
44
+ result.append(add_matrices((x[i] - y[i]) + y, x))
45
+ result = list(map(tuple, result))
46
+ if unique:
47
+ result = list(set(result))
48
+ if sort:
49
+ result = sort_line_segment(result, tuple(start_point))
50
+ return np.array(result)
51
+
52
+
53
+ def sort_line_segment(points : list[tuple], start_point : tuple) -> list:
54
+ points.remove(start_point)
55
+ stack = [start_point]
56
+ while points:
57
+ # The line segments in tropical geometry have limited slopes and are always convex
58
+ # Therefore the closest point (in Euclidean sense) is the next point in order
59
+ distances = {
60
+ math.sqrt(
61
+ sum(
62
+ (p[i] - stack[-1][i])**2 for i in range(len(start_point))
63
+ )
64
+ ): p for p in points
65
+ }
66
+ stack.append(distances[min(distances.keys())])
67
+ points.remove(stack[-1])
68
+ return stack
69
+
70
+
71
+ class ConvexCone:
72
+
73
+ def __init__(self, *vectors : Collection[float|int]) -> None:
74
+ validate_domain(vectors)
75
+ self.vectors = np.array(vectors) # generators of the cone
76
+ self.vector_count = self.vectors.shape[0]
77
+ self.dimensions = self.vectors.shape[1]
78
+
79
+ def get_point(self, constants : Collection[float|int]) -> np.ndarray:
80
+ if len(constants) != self.vector_count:
81
+ raise ValueError('The number of constants not equal the number of generators of the cone.')
82
+ result = np.array([-math.inf for _ in range(self.dimensions)])
83
+ for constant, vector in zip(constants, self.vectors):
84
+ result = add_matrices(result, vector + constant) # Addition here is tropical multiplication
85
+ return result
86
+
87
+ def sample_points(self, constants_collection : Collection[np.ndarray]) -> np.ndarray:
88
+ result = []
89
+ grid = np.meshgrid(*constants_collection)
90
+ for constants in np.nditer(grid):
91
+ result.append(self.get_point([float(s) for s in constants]))
92
+ result = np.array(result)
93
+ return result
94
+
95
+
96
+ class Hyperplane:
97
+ """ An implementation of a tropical hyperplane structure. """
98
+
99
+ def __init__(self, *coefficients : float) -> None:
100
+ validate_domain(coefficients)
101
+ self.coefficients = coefficients
102
+ self.dimension = len(coefficients)
103
+
104
+ def get_value(self, point : Collection[float]) -> float:
105
+ """ Calculate the value which the hyperplane achieves at a given point. """
106
+ if len(point) != self.dimension:
107
+ raise ValueError('The amount of the point\'s coordinates and the coefficients differs.')
108
+ result = add(*[mult(c, p) for c, p in zip(self.coefficients, point)])
109
+ return result
110
+
111
+ def get_apex(self) -> tuple:
112
+ """ Returns the coordinates of the point that serves as the apex of the hyperplane. """
113
+ return tuple([-coefficient for coefficient in self.coefficients])
114
+
115
+
116
+ def hyperplane_from_apex(point : Collection[float|int]) -> Hyperplane:
117
+ return Hyperplane(*[-coordinate for coordinate in point])
118
+
119
+
120
+ class AbstractPolytope:
121
+ """ An implementation of a tropical polytope structure. """
122
+
123
+ def __init__(self, faces_collection : list, normalize_points : bool = False) -> None:
124
+ if normalize_points:
125
+ faces_collection[0] = [(0, *face) for face in faces_collection[0]]
126
+ self.dimension = len(faces_collection[0][0]) - 1 # The dimension is defined by the coordinates of the first point
127
+ self.structure = {}
128
+ for rank, faces in enumerate(faces_collection):
129
+ validate_domain(faces)
130
+ self.structure[rank] = np.array(faces)
131
+ self._validate_vertices_dimension()
132
+
133
+ def _validate_vertices_dimension(self) -> None:
134
+ for vertex in self.structure[0]:
135
+ if len(vertex) - 1 != self.dimension:
136
+ raise ValueError('Incorrect dimension of the vertices\' coordinates.')
137
+
138
+ @property
139
+ def vertices(self) -> np.ndarray:
140
+ """ Returns all points generating the tropical polytope. """
141
+ return self.structure[0]
142
+
143
+ @property
144
+ def pseudovertices(self) -> np.ndarray:
145
+ """ Returns all points that are generated by the tropical polytope. """
146
+ result = []
147
+ for line_segment in self.get_all_line_segments():
148
+ for point in line_segment:
149
+ if not np.any(np.all(self.vertices == point, axis=1)): # if point not in vertices
150
+ result.append(point)
151
+ return np.array(result)
152
+
153
+ @property
154
+ def edges(self) -> list:
155
+ """ Returns all the edges (given as a pair of points) which build the tropical polytope. """
156
+ result = []
157
+ for vertices_identifiers in self.structure[1]:
158
+ result.append([])
159
+ for identifier in vertices_identifiers:
160
+ result[-1].append(self.vertices[identifier])
161
+ return result
162
+
163
+ def get_line_segments(self, edge_index : int, sort : bool = True) -> np.ndarray:
164
+ """ Returns a list of points belonging to a line segment of the polytope. """
165
+ start_point, end_point = self.edges[edge_index]
166
+ return np.array(line_segment(start_point, end_point, sort=sort))
167
+
168
+ def get_all_line_segments(self) -> list:
169
+ """ Returns all line segments building the polytope. """
170
+ result = []
171
+ for i in range(len(self.structure[1])):
172
+ result.append(self.get_line_segments(i, sort=True))
173
+ return result
174
+
175
+ def _verify_facet_definition(self, point : Collection) -> bool:
176
+ """ Verify if a point is facet-defining in the polynomial. """
177
+ T = point_type(point, self.vertices)
178
+ union = []
179
+ for t in T:
180
+ union.extend(t)
181
+ union = list(set(union))
182
+ if len(union) >= self.dimension:
183
+ return True
184
+ else:
185
+ return False
186
+
187
+ def get_apices(self) -> list:
188
+ """ Returns an array of apices corresponding to the polynomial-defining half-spaces. """
189
+ result = []
190
+ for pseudovertex in self.pseudovertices:
191
+ if self._verify_facet_definition(pseudovertex):
192
+ result.append(pseudovertex)
193
+ return result
194
+
195
+ def get_hyperplanes(self) -> list:
196
+ """ Returns a list of the facet-defining hyperplanes. """
197
+ result = []
198
+ for apex in self.get_apices():
199
+ result.append(hyperplane_from_apex(apex))
200
+ return result
201
+
202
+
203
+ class Polytope2D(AbstractPolytope):
204
+
205
+ def __init__(self, *points : Collection) -> None:
206
+ self.dimension = len(points[0]) - 1
207
+ faces_collection = [list(points)]
208
+ edges = []
209
+ for i in range(1, len(points)):
210
+ edges.append((i - 1, i))
211
+ edges.append((len(points) - 1, 0))
212
+ faces_collection.append(edges)
213
+ faces_collection.append([tuple(range(len(edges)))])
214
+ super().__init__(faces_collection)
@@ -0,0 +1,245 @@
1
+ import numpy as np
2
+
3
+ import math
4
+ import string
5
+
6
+ from .domain import validate_domain
7
+ from .. import utils
8
+
9
+
10
+ def add(*args : float) -> float:
11
+ validate_domain(args)
12
+ return max(args)
13
+
14
+
15
+ def mult(*args : float) -> float:
16
+ validate_domain(args)
17
+ return sum(args) if -math.inf not in args else -math.inf
18
+
19
+
20
+ def power(a : float, k : int) -> float:
21
+ return mult(*[a for _ in range(k)])
22
+
23
+
24
+ def modulo(a : float, t : int) -> float:
25
+ validate_domain([a, t])
26
+ if a < 0 or t < 0:
27
+ raise ValueError('The modulo operator is only defined for positive numbers.')
28
+ if a == -math.inf:
29
+ return -math.inf
30
+ if a == 0:
31
+ return 0
32
+ if t == -math.inf or t == 0:
33
+ return a
34
+ return a - (a // t) * t
35
+
36
+
37
+ def add_matrices(A : np.ndarray, B : np.ndarray) -> np.ndarray:
38
+ validate_domain([A, B])
39
+ return np.maximum(A, B)
40
+
41
+
42
+ def mult_matrices(A : np.ndarray, B : np.ndarray) -> np.ndarray:
43
+ if A.shape[1] != B.shape[0]:
44
+ raise ValueError('Given matrices are not of MxN and NxP shapes.')
45
+ result = np.zeros((A.shape[0], B.shape[1]))
46
+ for i in range(A.shape[0]):
47
+ for j in range(B.shape[1]):
48
+ result[i, j] = add(*[mult(A[i, k], B[k, j]) for k in range(A.shape[1])])
49
+ return result
50
+
51
+
52
+ def power_matrix(A : np.ndarray, k : int) -> np.ndarray:
53
+ if k == 0:
54
+ result = unit_matrix(A.shape[0], A.shape[1])
55
+ else:
56
+ result = A.copy()
57
+ for _ in range(k):
58
+ result = mult_matrices(A, result)
59
+ return result
60
+
61
+
62
+ def modulo_matrices(A : np.ndarray, b : np.ndarray) -> np.ndarray:
63
+ if b.shape[1] != 1:
64
+ raise ValueError('Given matrix b is not a vertical vector of shape Mx1')
65
+ if A.shape[0] != b.shape[0]:
66
+ raise ValueError('Given matrix b does not have an Mx1 shape against the MxN matrix A.')
67
+ if np.any(A < 0) or np.any(b < 0):
68
+ raise ValueError('Given matrices contain negative values.')
69
+ result = np.zeros(A.shape)
70
+ for i in range(A.shape[0]):
71
+ for j in range(A.shape[1]):
72
+ result[i, j] = modulo(A[i, j], b[i])
73
+ return result
74
+
75
+
76
+ def mult_arrays(A : np.ndarray, B : np.ndarray) -> np.ndarray:
77
+ """
78
+ Performs arctic tensor multiplication of NumPy arrays of any shape.
79
+ The operation is defined as:
80
+
81
+ A_{i_0, ..., i_k} * B_{j_0, ..., j_k} = C_{i_0, ..., i_k, j_0, ..., j_k}
82
+
83
+ where each element of C is calculated by arctic multiplication of the values in the arrays.
84
+ """
85
+ result = np.zeros((*A.shape, *B.shape))
86
+ for i, value in np.ndenumerate(A):
87
+ for j, other_value in np.ndenumerate(B):
88
+ result[*i, *j] = mult(value, other_value)
89
+ return result
90
+
91
+
92
+ def unit_matrix(width : int, height : int) -> np.ndarray:
93
+ result = np.eye(width, height)
94
+ result[result == 0] = -math.inf
95
+ result[result == 1] = 0
96
+ return result
97
+
98
+
99
+ def kleene_star(A : np.ndarray, iterations : int = 1000) -> np.ndarray:
100
+ if A.shape[0] != A.shape[1]:
101
+ raise ValueError('Matrix is not square.')
102
+ series = [
103
+ unit_matrix(A.shape[0], A.shape[1]),
104
+ A.copy()
105
+ ]
106
+ result = add_matrices(series[0], series[1])
107
+ for i in range(iterations):
108
+ series.append(power_matrix(A, i))
109
+ result = add_matrices(result, series[-1])
110
+ if np.all(series[-1] - series[-2] > 0): # If the values of the matrix are growing
111
+ break
112
+ return result
113
+
114
+
115
+ def kleene_plus(A : np.ndarray, iterations : int = 1000) -> np.ndarray:
116
+ if A.shape[0] != A.shape[1]:
117
+ raise ValueError('Matrix is not square.')
118
+ series = [A.copy()]
119
+ result = series[0]
120
+ for i in range(1, iterations):
121
+ series.append(power_matrix(A, i))
122
+ result = add_matrices(result, series[-1])
123
+ if np.all(series[-1] - series[-2] > 0): # If the values of the matrix are growing
124
+ break
125
+ return result
126
+
127
+
128
+ def power_algorithm(A : np.ndarray, x_0 : np.ndarray|None = None, iterations : int = 1000) -> tuple:
129
+ if x_0 is None:
130
+ x_0 = np.ones((A.shape[1], 1))
131
+ xs = [x_0]
132
+ for i in range(iterations):
133
+ xs.append(mult_matrices(A, xs[i]))
134
+ for j in range(len(xs) - 1):
135
+ if len(np.unique(xs[-1] - xs[j])) == 1:
136
+ p = len(xs) - 1
137
+ q = j
138
+ c = float(np.unique(xs[-1] - xs[j])[0])
139
+ return p, q, c, xs
140
+ raise ValueError(f'Unable to find the values using the power algorithm within {iterations} iterations.')
141
+
142
+
143
+ def eigenvalue(A : np.ndarray) -> float:
144
+ p, q, c, _ = power_algorithm(A)
145
+ return c / (p - q)
146
+
147
+
148
+ def eigenvector(A : np.ndarray) -> np.ndarray:
149
+ p, q, c, xs = power_algorithm(A)
150
+ eigenvalue = c / (p - q)
151
+ result = np.ones_like(xs[0]) * math.inf
152
+ for i in range(1, p - q + 1):
153
+ result = add_matrices(result, power(eigenvalue, (p - q - i)) + xs[q + i - 1])
154
+ return result
155
+
156
+
157
+ class MultivariatePolynomial:
158
+ """ An implementation of a tropical polynomial with multiple variables. """
159
+
160
+ def __init__(self, coefficients : np.ndarray) -> None:
161
+ validate_domain(coefficients)
162
+ self.coefficients = coefficients
163
+ self.dimensions = len(self.coefficients.shape) + 1
164
+ self._symbols = string.ascii_lowercase
165
+
166
+ def __call__(self, *variables : float) -> float:
167
+ if len(variables) != self.dimensions - 1:
168
+ raise ValueError('The amount of variables and coefficients differs.')
169
+ result = [-math.inf]
170
+ for indices, coefficient in np.ndenumerate(self.coefficients):
171
+ powers : list[float] = []
172
+ for variable_index, i in enumerate(indices):
173
+ powers.append(power(variables[variable_index], i))
174
+ result.append(mult(coefficient, *powers))
175
+ result = add(*result)
176
+ return float(result)
177
+
178
+ def __str__(self) -> str:
179
+ result = ''
180
+ for indices, coefficient in np.ndenumerate(self.coefficients):
181
+ if coefficient.is_integer():
182
+ result += '(' + str(int(coefficient))
183
+ elif coefficient > -math.inf:
184
+ result += '(' + str(coefficient)
185
+ else:
186
+ result += '(-∞'
187
+ for variable_index, i in enumerate(indices):
188
+ if i > 1:
189
+ result += ' * ' + self._symbols[variable_index] + '^' + str(i)
190
+ elif i == 1:
191
+ result += ' * ' + self._symbols[variable_index]
192
+ result += ') + '
193
+ return result[:-3]
194
+
195
+ def get_linear_hyperplanes(self) -> list[list[float|int]]:
196
+ """ Returns a list of coefficients of a linear equation for every hyperplane building the polynomial. """
197
+ result = []
198
+ for indices, coefficient in np.ndenumerate(self.coefficients):
199
+ if coefficient == -math.inf:
200
+ continue
201
+ hyperplane = [float(coefficient)]
202
+ hyperplane.extend(indices)
203
+ hyperplane.append(1) # The coefficient of the last dimension (e.g. Z in 3D)
204
+ result.append(
205
+ list(
206
+ map(
207
+ lambda x: int(x) if x.is_integer() else float(x),
208
+ hyperplane
209
+ )
210
+ )
211
+ )
212
+ return result
213
+
214
+
215
+ class Polynomial(MultivariatePolynomial):
216
+ """ An implementation of a tropical polynomial with a single variable. """
217
+
218
+ def __init__(self, *coefficients : float|int) -> None:
219
+ validate_domain(coefficients)
220
+ super().__init__(np.array(coefficients))
221
+
222
+ def get_line_intersections(self) -> list[list[float|int]]:
223
+ """ Returns a list of intersection points for the lines building the polynomial. """
224
+ result = []
225
+ lines = self.get_linear_hyperplanes() # Hyperplanes are lines in this case
226
+ for line in lines: # Change the form of the equation to a + bx from a + bx + cy
227
+ _ = line.pop()
228
+ lines = filter(lambda x: len(x) == 2, utils.powerset(lines))
229
+ for line_1, line_2 in lines:
230
+ point = [(line_2[0] - line_1[0]) / (line_1[1] - line_2[1])]
231
+ point.append(line_1[0] + line_1[1] * point[0])
232
+ result.append(tuple(point))
233
+ result = list(filter(lambda point: round(point[1], 8) == round(self(point[0]), 8), result)) # Filter out the points not belonging to the polynomial
234
+ return result
235
+
236
+ def get_roots(self) -> tuple[list[float|int], list[int]]:
237
+ """ Returns lists of roots of the polynomial and of their respective ranks (the amount of monomials attaining the value). """
238
+ result = {}
239
+ points = self.get_line_intersections()
240
+ for point in points:
241
+ if point[0] not in result:
242
+ result[point[0]] = 1
243
+ else:
244
+ result[point[0]] += 1
245
+ return list(result.keys()), list(result.values())
@@ -0,0 +1,65 @@
1
+ import matplotlib.pyplot as plt
2
+ from matplotlib.ticker import MaxNLocator
3
+
4
+ from .geometry import AbstractPolytope, project_point
5
+
6
+
7
+ def hasse_diagram(polytope : AbstractPolytope) -> None:
8
+ """ Draws and shows the Hasse diagram of the given polytope using matplotlib. """
9
+ _, ax = plt.subplots()
10
+ ax.set_xticks([])
11
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
12
+ ax.yaxis.tick_right()
13
+ ax.yaxis.set_label_position('right')
14
+ ax.set_ylabel('Rank')
15
+ for spine in ['top', 'left', 'bottom']:
16
+ ax.spines[spine].set_visible(False)
17
+ width = max(map(len, polytope.structure.values()))
18
+ layers = {}
19
+ for rank, layer in polytope.structure.items():
20
+ points = []
21
+ for i, connections in enumerate(layer):
22
+ x = (i * width) / (len(layer) - 1) if len(layer) > 1 else width / 2
23
+ points.append((x, rank))
24
+ if rank > 0:
25
+ for connection in connections:
26
+ ax.plot(
27
+ [x, layers[rank - 1][connection][0]],
28
+ [rank, layers[rank - 1][connection][1]],
29
+ color='black'
30
+ )
31
+ layers[rank] = points
32
+ for i, point in enumerate(points):
33
+ ax.text(
34
+ point[0], point[1], str(i),
35
+ ha='center', va='center', fontsize=10,
36
+ bbox={
37
+ 'facecolor': 'white',
38
+ 'edgecolor': 'black',
39
+ 'boxstyle': 'circle',
40
+ }
41
+ )
42
+ plt.show()
43
+
44
+
45
+ def draw_polytope2D(polytope : AbstractPolytope) -> None:
46
+ """ Draws a given polytope on a 2-dimensional surface using matplotlib. """
47
+ if polytope.dimension < 2:
48
+ raise NotImplementedError('Projection of polytopes of lesser dimensions not implemented currently.')
49
+ if polytope.dimension != 2:
50
+ raise ValueError('The polytope cannot be projected onto a 2-dimensional surface.')
51
+ vertices = list(map(project_point, polytope.vertices))
52
+ plt.scatter(
53
+ [vertex[0] for vertex in vertices],
54
+ [vertex[1] for vertex in vertices],
55
+ color='black'
56
+ )
57
+ line_segments = polytope.get_all_line_segments()
58
+ for line_segment in line_segments:
59
+ line_segment = list(map(project_point, line_segment))
60
+ plt.plot(
61
+ [vertex[0] for vertex in line_segment],
62
+ [vertex[1] for vertex in line_segment],
63
+ color='black'
64
+ )
65
+ plt.show()
@@ -0,0 +1,4 @@
1
+ from .minplus import *
2
+ from .domain import *
3
+ from . import geometry
4
+ from . import visualize
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+
3
+ import math
4
+ from typing import Any
5
+ from collections.abc import Collection
6
+
7
+
8
+ class TropicalNumberMeta(type):
9
+ def __instancecheck__(self, instance: Any, /) -> bool:
10
+ if isinstance(instance, float|int|np.floating|np.integer):
11
+ if instance > -math.inf:
12
+ return True
13
+ return False
14
+
15
+
16
+ class TropicalNumber(metaclass=TropicalNumberMeta):
17
+ pass
18
+
19
+
20
+ def validate_domain(value : Any) -> None:
21
+ if isinstance(value, Collection):
22
+ for item in value:
23
+ validate_domain(item)
24
+ return
25
+ if isinstance(value, TropicalNumber):
26
+ return
27
+ raise ValueError('Value out of domain.')