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 +46 -0
- mplusa-0.0.4/README.md +12 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/pyproject.toml +2 -2
- mplusa-0.0.4/src/mplusa/maxplus/__init__.py +4 -0
- mplusa-0.0.4/src/mplusa/maxplus/domain.py +27 -0
- mplusa-0.0.4/src/mplusa/maxplus/geometry.py +214 -0
- {mplusa-0.0.3/src/mplusa → mplusa-0.0.4/src/mplusa/maxplus}/maxplus.py +94 -49
- mplusa-0.0.4/src/mplusa/maxplus/visualize.py +65 -0
- mplusa-0.0.4/src/mplusa/minplus/__init__.py +4 -0
- mplusa-0.0.4/src/mplusa/minplus/domain.py +27 -0
- mplusa-0.0.4/src/mplusa/minplus/geometry.py +214 -0
- {mplusa-0.0.3/src/mplusa → mplusa-0.0.4/src/mplusa/minplus}/minplus.py +93 -48
- mplusa-0.0.4/src/mplusa/minplus/visualize.py +65 -0
- mplusa-0.0.4/src/mplusa.egg-info/PKG-INFO +46 -0
- mplusa-0.0.4/src/mplusa.egg-info/SOURCES.txt +20 -0
- mplusa-0.0.3/PKG-INFO +0 -98
- mplusa-0.0.3/README.md +0 -64
- mplusa-0.0.3/src/mplusa.egg-info/PKG-INFO +0 -98
- mplusa-0.0.3/src/mplusa.egg-info/SOURCES.txt +0 -12
- {mplusa-0.0.3 → mplusa-0.0.4}/LICENSE +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/setup.cfg +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/src/mplusa/__init__.py +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/src/mplusa/utils.py +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/src/mplusa.egg-info/dependency_links.txt +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/src/mplusa.egg-info/requires.txt +0 -0
- {mplusa-0.0.3 → mplusa-0.0.4}/src/mplusa.egg-info/top_level.txt +0 -0
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
|
+
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://
|
|
21
|
+
documentation = "https://maksymilian-wiekiera.fyi/mplusa/0.0.4.html"
|
|
@@ -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
|
|
6
|
+
from .domain import validate_domain
|
|
7
|
+
from .. import utils
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def add(*args) -> float:
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
91
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,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.')
|