mplusa 0.0.3__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,6 +1,6 @@
1
1
  [project]
2
2
  name = "mplusa"
3
- version = "0.0.3"
3
+ version = "0.0.4"
4
4
  authors = [
5
5
  {name="Maksymilian Wiekiera", email="maksymilian3563@gmail.com"}
6
6
  ]
@@ -18,4 +18,4 @@ license = {file="LICENSE"}
18
18
 
19
19
  [project.urls]
20
20
  homepage = "https://github.com/Hadelekw/mplusa"
21
- documentation = "https://hadelekw.github.io/mplusa-docs.html"
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)
@@ -3,28 +3,26 @@ import numpy as np
3
3
  import math
4
4
  import string
5
5
 
6
- from . import utils
6
+ from .domain import validate_domain
7
+ from .. import utils
7
8
 
8
9
 
9
- def add(*args) -> float:
10
- if math.inf in args:
11
- raise ValueError('Value out of domain.')
10
+ def add(*args : float) -> float:
11
+ validate_domain(args)
12
12
  return max(args)
13
13
 
14
14
 
15
- def mult(*args) -> float:
16
- if math.inf in args:
17
- raise ValueError('Value out of domain.')
15
+ def mult(*args : float) -> float:
16
+ validate_domain(args)
18
17
  return sum(args) if -math.inf not in args else -math.inf
19
18
 
20
19
 
21
- def power(a : float,
22
- k : int) -> float:
20
+ def power(a : float, k : int) -> float:
23
21
  return mult(*[a for _ in range(k)])
24
22
 
25
23
 
26
- def modulo(a : float,
27
- t : int) -> float:
24
+ def modulo(a : float, t : int) -> float:
25
+ validate_domain([a, t])
28
26
  if a < 0 or t < 0:
29
27
  raise ValueError('The modulo operator is only defined for positive numbers.')
30
28
  if a == -math.inf:
@@ -36,20 +34,12 @@ def modulo(a : float,
36
34
  return a - (a // t) * t
37
35
 
38
36
 
39
- def add_matrices(A : np.ndarray,
40
- B : np.ndarray) -> np.ndarray:
41
- if A.shape != B.shape:
42
- raise ValueError('Given matrices have different shapes.')
43
- result = np.copy(A)
44
- shape = A.shape
45
- for i in range(shape[0]):
46
- for j in range(shape[1]):
47
- result[i, j] = add(result[i, j], B[i, j])
48
- return result
37
+ def add_matrices(A : np.ndarray, B : np.ndarray) -> np.ndarray:
38
+ validate_domain([A, B])
39
+ return np.maximum(A, B)
49
40
 
50
41
 
51
- def mult_matrices(A : np.ndarray,
52
- B : np.ndarray) -> np.ndarray:
42
+ def mult_matrices(A : np.ndarray, B : np.ndarray) -> np.ndarray:
53
43
  if A.shape[1] != B.shape[0]:
54
44
  raise ValueError('Given matrices are not of MxN and NxP shapes.')
55
45
  result = np.zeros((A.shape[0], B.shape[1]))
@@ -59,10 +49,7 @@ def mult_matrices(A : np.ndarray,
59
49
  return result
60
50
 
61
51
 
62
- def power_matrix(A : np.ndarray,
63
- k : int) -> np.ndarray:
64
- if np.any(np.diagonal(A) != 0):
65
- raise ValueError('Matrix contains non-zero values on the diagonal.')
52
+ def power_matrix(A : np.ndarray, k : int) -> np.ndarray:
66
53
  if k == 0:
67
54
  result = unit_matrix(A.shape[0], A.shape[1])
68
55
  else:
@@ -72,8 +59,7 @@ def power_matrix(A : np.ndarray,
72
59
  return result
73
60
 
74
61
 
75
- def modulo_matrices(A : np.ndarray,
76
- b : np.ndarray) -> np.ndarray:
62
+ def modulo_matrices(A : np.ndarray, b : np.ndarray) -> np.ndarray:
77
63
  if b.shape[1] != 1:
78
64
  raise ValueError('Given matrix b is not a vertical vector of shape Mx1')
79
65
  if A.shape[0] != b.shape[0]:
@@ -87,31 +73,92 @@ def modulo_matrices(A : np.ndarray,
87
73
  return result
88
74
 
89
75
 
90
- def unit_matrix(width : int,
91
- height : int) -> np.ndarray:
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:
92
93
  result = np.eye(width, height)
93
94
  result[result == 0] = -math.inf
94
95
  result[result == 1] = 0
95
96
  return result
96
97
 
97
98
 
98
- def kleene_star(A : np.ndarray,
99
- iterations : int = 1000) -> np.ndarray:
99
+ def kleene_star(A : np.ndarray, iterations : int = 1000) -> np.ndarray:
100
100
  if A.shape[0] != A.shape[1]:
101
101
  raise ValueError('Matrix is not square.')
102
102
  series = [
103
103
  unit_matrix(A.shape[0], A.shape[1]),
104
104
  A.copy()
105
105
  ]
106
- for _ in range(iterations):
107
- series.append(add_matrices(series[-1], mult_matrices(series[-1], series[-2])))
108
- return series[-1]
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
109
155
 
110
156
 
111
157
  class MultivariatePolynomial:
112
- """ An implementation of an arctic polynomial with multiple variables. """
158
+ """ An implementation of a tropical polynomial with multiple variables. """
113
159
 
114
160
  def __init__(self, coefficients : np.ndarray) -> None:
161
+ validate_domain(coefficients)
115
162
  self.coefficients = coefficients
116
163
  self.dimensions = len(self.coefficients.shape) + 1
117
164
  self._symbols = string.ascii_lowercase
@@ -121,7 +168,7 @@ class MultivariatePolynomial:
121
168
  raise ValueError('The amount of variables and coefficients differs.')
122
169
  result = [-math.inf]
123
170
  for indices, coefficient in np.ndenumerate(self.coefficients):
124
- powers = []
171
+ powers : list[float] = []
125
172
  for variable_index, i in enumerate(indices):
126
173
  powers.append(power(variables[variable_index], i))
127
174
  result.append(mult(coefficient, *powers))
@@ -145,7 +192,7 @@ class MultivariatePolynomial:
145
192
  result += ') + '
146
193
  return result[:-3]
147
194
 
148
- def get_hyperplanes(self) -> list:
195
+ def get_linear_hyperplanes(self) -> list[list[float|int]]:
149
196
  """ Returns a list of coefficients of a linear equation for every hyperplane building the polynomial. """
150
197
  result = []
151
198
  for indices, coefficient in np.ndenumerate(self.coefficients):
@@ -168,18 +215,16 @@ class MultivariatePolynomial:
168
215
  class Polynomial(MultivariatePolynomial):
169
216
  """ An implementation of a tropical polynomial with a single variable. """
170
217
 
171
- def __init__(self, *coefficients) -> None:
172
- for value in coefficients:
173
- if not isinstance(value, float|int) or value == math.inf:
174
- raise ValueError('Coefficient value out of domain.')
218
+ def __init__(self, *coefficients : float|int) -> None:
219
+ validate_domain(coefficients)
175
220
  super().__init__(np.array(coefficients))
176
221
 
177
- def get_line_intersections(self) -> list:
222
+ def get_line_intersections(self) -> list[list[float|int]]:
178
223
  """ Returns a list of intersection points for the lines building the polynomial. """
179
224
  result = []
180
- lines = self.get_hyperplanes() # Hyperplanes are lines in this case
225
+ lines = self.get_linear_hyperplanes() # Hyperplanes are lines in this case
181
226
  for line in lines: # Change the form of the equation to a + bx from a + bx + cy
182
- line.pop()
227
+ _ = line.pop()
183
228
  lines = filter(lambda x: len(x) == 2, utils.powerset(lines))
184
229
  for line_1, line_2 in lines:
185
230
  point = [(line_2[0] - line_1[0]) / (line_1[1] - line_2[1])]
@@ -188,12 +233,12 @@ class Polynomial(MultivariatePolynomial):
188
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
189
234
  return result
190
235
 
191
- def get_roots(self) -> tuple:
192
- """ Returns lists of roots of the polynomial and of their respective ranks (amount of monomials attaining the value). """
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). """
193
238
  result = {}
194
239
  points = self.get_line_intersections()
195
240
  for point in points:
196
- if not point[0] in result:
241
+ if point[0] not in result:
197
242
  result[point[0]] = 1
198
243
  else:
199
244
  result[point[0]] += 1
@@ -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.')