mplusa 0.0.1__py3-none-any.whl → 0.0.4__py3-none-any.whl
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/maxplus/__init__.py +4 -0
- mplusa/maxplus/domain.py +27 -0
- mplusa/maxplus/geometry.py +214 -0
- mplusa/maxplus/maxplus.py +245 -0
- mplusa/maxplus/visualize.py +65 -0
- mplusa/minplus/__init__.py +4 -0
- mplusa/minplus/domain.py +27 -0
- mplusa/minplus/geometry.py +214 -0
- mplusa/minplus/minplus.py +245 -0
- mplusa/minplus/visualize.py +65 -0
- mplusa/utils.py +5 -0
- mplusa-0.0.4.dist-info/METADATA +46 -0
- mplusa-0.0.4.dist-info/RECORD +17 -0
- {mplusa-0.0.1.dist-info → mplusa-0.0.4.dist-info}/WHEEL +1 -1
- mplusa/maxplus.py +0 -161
- mplusa/minplus.py +0 -161
- mplusa-0.0.1.dist-info/METADATA +0 -84
- mplusa-0.0.1.dist-info/RECORD +0 -8
- {mplusa-0.0.1.dist-info → mplusa-0.0.4.dist-info/licenses}/LICENSE +0 -0
- {mplusa-0.0.1.dist-info → mplusa-0.0.4.dist-info}/top_level.txt +0 -0
|
@@ -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 .minplus 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 min(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.minimum(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 tropical 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 tropical 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()
|
mplusa/utils.py
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.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
mplusa/__init__.py,sha256=8D1JvAZfjHkQ1DOqrxLyXWMEfROgGwf8Y41-rSX51cY,44
|
|
2
|
+
mplusa/utils.py,sha256=Q5gXJJ06N1Nt65Ept3qRreWvyBC552DsiEkAHHT2IWE,168
|
|
3
|
+
mplusa/maxplus/__init__.py,sha256=EWuzzBdP7UJqWuJGTz3OTAMbN_gOvO3QOWOdVFLTWHA,92
|
|
4
|
+
mplusa/maxplus/domain.py,sha256=h5ggcrlPWH8b1EsxJ1OZmIt49IGcOqQHYqqYltUaXoE,654
|
|
5
|
+
mplusa/maxplus/geometry.py,sha256=IEnRup5wLjB0A2o9MWVsRw6V0gMCp7lVC7evlk72PDo,8097
|
|
6
|
+
mplusa/maxplus/maxplus.py,sha256=mqfbH-GNsIOW0IicwcK6y6VMBmBVRZGzD8k-69ZDPTo,8758
|
|
7
|
+
mplusa/maxplus/visualize.py,sha256=HfMyVCsw3sB6o-5wPGHaIEiePwaDRXb9pynAqVoB4l0,2467
|
|
8
|
+
mplusa/minplus/__init__.py,sha256=w2DjNCWlMKCd9Lxr7PSl1CXLPxbFec84OrpyhRPCVB8,92
|
|
9
|
+
mplusa/minplus/domain.py,sha256=cepeADI6_pY5eF7ufVH4udKN8J_7Z--Qhi6ONDe-QCQ,663
|
|
10
|
+
mplusa/minplus/geometry.py,sha256=InE_5tUkMbQl9nalgsaDb0eBrrWmLiAPbmNxOCKEnAw,8096
|
|
11
|
+
mplusa/minplus/minplus.py,sha256=ANNZBh4GLpDsAwVt7O91_W3NuNds9jwEJki8o35cBvY,8752
|
|
12
|
+
mplusa/minplus/visualize.py,sha256=HfMyVCsw3sB6o-5wPGHaIEiePwaDRXb9pynAqVoB4l0,2467
|
|
13
|
+
mplusa-0.0.4.dist-info/licenses/LICENSE,sha256=x1S-x_tM1tAmndGsdQKT4DU9LnstfQRgS4befak4XdA,1063
|
|
14
|
+
mplusa-0.0.4.dist-info/METADATA,sha256=qWqe_Ca3GvdEjJJPNYIx6VvxYzIninQcz3p83iEpMQI,2675
|
|
15
|
+
mplusa-0.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
+
mplusa-0.0.4.dist-info/top_level.txt,sha256=W5b7P8CkZ-DB3-2K0Rcf0T0tpEuWLCbZBaVgrd7FEcM,7
|
|
17
|
+
mplusa-0.0.4.dist-info/RECORD,,
|