polyany 0.3.0__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.
- polyany/__init__.py +3 -0
- polyany/exponents.py +23 -0
- polyany/polynomial.py +904 -0
- polyany/py.typed +0 -0
- polyany/types.py +10 -0
- polyany-0.3.0.dist-info/METADATA +90 -0
- polyany-0.3.0.dist-info/RECORD +9 -0
- polyany-0.3.0.dist-info/WHEEL +4 -0
- polyany-0.3.0.dist-info/licenses/LICENSE +28 -0
polyany/__init__.py
ADDED
polyany/exponents.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_quadratic_exponents(n_vars: int) -> np.ndarray:
|
|
5
|
+
eye = np.eye(n_vars, dtype=np.int16)
|
|
6
|
+
i, j = np.triu_indices(n_vars)
|
|
7
|
+
|
|
8
|
+
return eye[i] + eye[j]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def domain_expansion(exponents: np.ndarray, expanded_n_vars: int) -> np.ndarray:
|
|
12
|
+
exponents = exponents.copy()
|
|
13
|
+
extra_vars = expanded_n_vars - exponents.shape[1]
|
|
14
|
+
|
|
15
|
+
if extra_vars > 0:
|
|
16
|
+
exponents = np.hstack(
|
|
17
|
+
(
|
|
18
|
+
exponents,
|
|
19
|
+
np.zeros(shape=(len(exponents), extra_vars), dtype=np.int16),
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return exponents
|
polyany/polynomial.py
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
from .exponents import domain_expansion, get_quadratic_exponents
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
12
|
+
from .types import Algebraic, Scalar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Polynomial:
|
|
16
|
+
"""A multivariate polynomial class.
|
|
17
|
+
|
|
18
|
+
Represents a multivariate polynomial in the form:
|
|
19
|
+
|
|
20
|
+
P(X) = โ c_i * x_1^e_i1 * x_2^e_i2 * ... * x_n^e_in
|
|
21
|
+
|
|
22
|
+
where `c_i` are the coefficients and `e_ji` are the exponents of each monomial.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
exponents : ArrayLike
|
|
27
|
+
A nested sequence or a NumPy 2D-array with shape (n_monomials, n_vars),
|
|
28
|
+
where each row contains the exponents of one monomial.
|
|
29
|
+
The order of variables is assumed to be increasing, i.e.,
|
|
30
|
+
[x_1, x_2, ..., x_n].
|
|
31
|
+
coefficients : ArrayLike
|
|
32
|
+
A sequence or a NumPy 1D-array with shape (n_monomials,). Containing the
|
|
33
|
+
corresponding scalar multipliers of each monomial.
|
|
34
|
+
|
|
35
|
+
Attributes
|
|
36
|
+
----------
|
|
37
|
+
n_vars : int
|
|
38
|
+
Number of variables in the polynomial.
|
|
39
|
+
degree : int
|
|
40
|
+
Total degree of the polynomial.
|
|
41
|
+
exponents : np.ndarray
|
|
42
|
+
A NumPy 2D-array representing the exponents
|
|
43
|
+
of the polynomial.
|
|
44
|
+
coefficients : np.ndarray
|
|
45
|
+
A NumPy 1D-array with the corresponding coefficients.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
TypeError
|
|
50
|
+
- If the input exponents cannot be safely converted to a
|
|
51
|
+
NumPy 2D-array of integers.
|
|
52
|
+
- If the input coefficients cannot be safely converted to a
|
|
53
|
+
NumPy 1D-array of floats.
|
|
54
|
+
|
|
55
|
+
ValueError
|
|
56
|
+
- If the number of exponents does not match the number of coefficients.
|
|
57
|
+
- If the input arrays dimensions are inconsistent.
|
|
58
|
+
- If the input exponents rows are not unique.
|
|
59
|
+
- If any input exponent entry is negative.
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
The current implementation allows coefficients to be complex numbers,
|
|
64
|
+
but complex polynomials are not yet officially supported and may produce
|
|
65
|
+
unexpected behavior.
|
|
66
|
+
|
|
67
|
+
Although attributes are publicly accessible, modifying them directly may lead
|
|
68
|
+
to bugs and unexpected behavior.
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
>>> from polyany import Polynomial
|
|
73
|
+
|
|
74
|
+
Create the polynomial: ``5*x_1**2*x_2*x_3**4*x_5 + 3*x_1*x_2 + 4*x_4**4*x_5**3``
|
|
75
|
+
|
|
76
|
+
>>> exponents = [[1, 1, 0, 0, 0],
|
|
77
|
+
... [0, 0, 0, 4, 3],
|
|
78
|
+
... [2, 1, 4, 0, 1]]
|
|
79
|
+
>>> coefficients = [3, 4, 5]
|
|
80
|
+
>>> Polynomial(exponents, coefficients)
|
|
81
|
+
3*x_1*x_2 + 4*x_4^4*x_5^3 + 5*x_1^2*x_2*x_3^4*x_5
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, exponents: ArrayLike, coefficients: ArrayLike) -> None:
|
|
85
|
+
input_exponents, input_coefficients = self._sanitize_inputs(
|
|
86
|
+
exponents, coefficients
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.n_vars = input_exponents.shape[1]
|
|
90
|
+
self.degree = np.max(np.sum(input_exponents, axis=1)).item()
|
|
91
|
+
|
|
92
|
+
self.exponents, self.coefficients = self._sort_inputs(
|
|
93
|
+
input_exponents, input_coefficients
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _sanitize_inputs(
|
|
97
|
+
self, input_exponents: ArrayLike, input_coefficients: ArrayLike
|
|
98
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
99
|
+
try:
|
|
100
|
+
converted_coefficients = np.asarray(input_coefficients).astype(
|
|
101
|
+
dtype=np.float64, casting="safe"
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
msg = (
|
|
105
|
+
"Coefficients must be safe-convertible to NumPy 1D-arrays "
|
|
106
|
+
"with float entries."
|
|
107
|
+
)
|
|
108
|
+
raise TypeError(msg) from e
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
converted_exponents = np.asarray(input_exponents).astype(
|
|
112
|
+
dtype=np.int_, casting="safe"
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
msg = (
|
|
116
|
+
"Exponents must be safe-convertible to NumPy 2D-arrays "
|
|
117
|
+
"with int entries."
|
|
118
|
+
)
|
|
119
|
+
raise TypeError(msg) from e
|
|
120
|
+
|
|
121
|
+
if converted_exponents.ndim != 2:
|
|
122
|
+
msg = f"Exponents must have 2 dimensions, got {converted_exponents.ndim}."
|
|
123
|
+
raise ValueError(msg)
|
|
124
|
+
|
|
125
|
+
if converted_coefficients.ndim != 1:
|
|
126
|
+
msg = (
|
|
127
|
+
"Coefficients must have 1 dimension, "
|
|
128
|
+
f"got {converted_coefficients.ndim}."
|
|
129
|
+
)
|
|
130
|
+
raise ValueError(msg)
|
|
131
|
+
|
|
132
|
+
if len(np.unique(converted_exponents, axis=0)) != len(converted_exponents):
|
|
133
|
+
msg = "Exponents entries must be unique."
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
|
|
136
|
+
if len(converted_exponents) != len(converted_coefficients):
|
|
137
|
+
msg = (
|
|
138
|
+
"Number of exponents and coefficients must match, "
|
|
139
|
+
f"got {converted_exponents.shape[0]} exponents / "
|
|
140
|
+
f"{converted_coefficients.shape[0]} coefficients."
|
|
141
|
+
)
|
|
142
|
+
raise ValueError(msg)
|
|
143
|
+
|
|
144
|
+
if not np.all(converted_exponents >= 0):
|
|
145
|
+
msg = (
|
|
146
|
+
"PolyAny is not yet able to handle nonlinear polynomials. "
|
|
147
|
+
"Make sure that all exponents are >= 0."
|
|
148
|
+
)
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
|
|
151
|
+
return converted_exponents, converted_coefficients
|
|
152
|
+
|
|
153
|
+
def __repr__(self) -> str:
|
|
154
|
+
# TODO(@ximiraxelo): truncate output for large polynomials
|
|
155
|
+
|
|
156
|
+
monomials: list[str] = []
|
|
157
|
+
for exponent, coefficient in zip(
|
|
158
|
+
self.exponents, self.coefficients, strict=True
|
|
159
|
+
):
|
|
160
|
+
if coefficient == 0:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
variables = "*".join(
|
|
164
|
+
[
|
|
165
|
+
f"x_{idx + 1}^{deg}" if deg > 1 else f"x_{idx + 1}"
|
|
166
|
+
for idx, deg in enumerate(exponent)
|
|
167
|
+
if deg > 0
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if float(coefficient).is_integer():
|
|
172
|
+
coef_value = abs(int(coefficient))
|
|
173
|
+
else:
|
|
174
|
+
coef_value = abs(coefficient)
|
|
175
|
+
|
|
176
|
+
coef_str = "" if coef_value == 1 and variables else str(coef_value)
|
|
177
|
+
|
|
178
|
+
term = f"{coef_str}{'*' if variables and coef_str else ''}{variables}"
|
|
179
|
+
sign = " - " if coefficient < 0 else (" + " if monomials else "")
|
|
180
|
+
|
|
181
|
+
monomials.append(f"{sign}{term}" if sign else term)
|
|
182
|
+
|
|
183
|
+
if not monomials:
|
|
184
|
+
return "0"
|
|
185
|
+
|
|
186
|
+
monomials[0] = monomials[0].replace(" ", "")
|
|
187
|
+
|
|
188
|
+
return "".join(monomials)
|
|
189
|
+
|
|
190
|
+
def _sort_inputs(
|
|
191
|
+
self, exponents: np.ndarray, coefficients: np.ndarray
|
|
192
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
193
|
+
monomials_degree = np.sum(exponents, axis=1)
|
|
194
|
+
sorted_idx = np.lexsort((*exponents.T, monomials_degree))
|
|
195
|
+
|
|
196
|
+
return exponents[sorted_idx], coefficients[sorted_idx]
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def univariate(cls, coefficients: ArrayLike) -> Polynomial:
|
|
200
|
+
"""Creates a univariate polynomial from a coefficients vector
|
|
201
|
+
|
|
202
|
+
This classmethod is a convenient shortcut to construct a univariate polynomial
|
|
203
|
+
from a coefficients vector.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
coefficients : ArrayLike
|
|
208
|
+
The coefficients of the univariate polynomial, associated with increasing
|
|
209
|
+
powers of the variable `x_1`.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
Polynomial
|
|
214
|
+
A univariate polynomial.
|
|
215
|
+
|
|
216
|
+
Raises
|
|
217
|
+
------
|
|
218
|
+
ValueError
|
|
219
|
+
- If `coefficients` does not have exactly one dimension.
|
|
220
|
+
|
|
221
|
+
Examples
|
|
222
|
+
--------
|
|
223
|
+
>>> Polynomial.univariate([1, 2, -3, -4, 5])
|
|
224
|
+
1 + 2*x_1 - 3*x_1^2 - 4*x_1^3 + 5*x_1^4
|
|
225
|
+
"""
|
|
226
|
+
converted_coefficients = np.asarray(coefficients)
|
|
227
|
+
|
|
228
|
+
if converted_coefficients.ndim != 1:
|
|
229
|
+
msg = (
|
|
230
|
+
f"Coefficients must have 1 dimension, got {converted_coefficients.ndim}"
|
|
231
|
+
)
|
|
232
|
+
raise ValueError(msg)
|
|
233
|
+
|
|
234
|
+
exponents = np.arange(0, len(converted_coefficients)).reshape(-1, 1)
|
|
235
|
+
|
|
236
|
+
return cls(exponents, coefficients)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def quadratic_form(cls, matrix: ArrayLike) -> Polynomial:
|
|
240
|
+
"""Creates a quadratic form from its associated symmetric matrix
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
matrix : ArrayLike
|
|
245
|
+
A nested sequence or a NumPy 2D array of shape (`n_vars`, `n_vars`) that
|
|
246
|
+
representing the symmetric matrix associated with the quadratic form.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
Polynomial
|
|
251
|
+
A second-degree homogeneous multivariate polynomial, i.e, a quadratic form.
|
|
252
|
+
|
|
253
|
+
Raises
|
|
254
|
+
------
|
|
255
|
+
TypeError
|
|
256
|
+
- If `matrix` is not safe-convertible to a
|
|
257
|
+
NumPy 2D array with float entries.
|
|
258
|
+
ValueError
|
|
259
|
+
- If `matrix` does not have 2 dimensions.
|
|
260
|
+
- If `matrix` is not square.
|
|
261
|
+
|
|
262
|
+
Warns
|
|
263
|
+
-----
|
|
264
|
+
UserWarning
|
|
265
|
+
- If `matrix`is not symmetric.
|
|
266
|
+
|
|
267
|
+
Notes
|
|
268
|
+
-----
|
|
269
|
+
If `matrix` is not symmetric, its symmetric part is used instead,
|
|
270
|
+
computed as `symmetric_part = (matrix + matrix.T) / 2`.
|
|
271
|
+
|
|
272
|
+
Examples
|
|
273
|
+
--------
|
|
274
|
+
>>> matrix = [[5, 3, 2],
|
|
275
|
+
... [3, 1, 0],
|
|
276
|
+
... [2, 0, 7]]
|
|
277
|
+
>>> Polynomial.quadratic_form(matrix)
|
|
278
|
+
5*x_1^2 + 6*x_1*x_2 + x_2^2 + 4*x_1*x_3 + 7*x_3^2
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
converted_matrix = np.asarray(matrix).astype(
|
|
282
|
+
dtype=np.float64, casting="safe", copy=True
|
|
283
|
+
)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
msg = (
|
|
286
|
+
"Matrix must be safe-convertible to NumPy 2D-array with float entries."
|
|
287
|
+
)
|
|
288
|
+
raise TypeError(msg) from e
|
|
289
|
+
|
|
290
|
+
if converted_matrix.ndim != 2:
|
|
291
|
+
msg = f"Matrix must have 2 dimensions, got {converted_matrix.ndim}"
|
|
292
|
+
raise ValueError(msg)
|
|
293
|
+
|
|
294
|
+
if converted_matrix.shape[0] != converted_matrix.shape[1]:
|
|
295
|
+
msg = f"Matrix must be square, got {converted_matrix.shape}"
|
|
296
|
+
raise ValueError(msg)
|
|
297
|
+
|
|
298
|
+
if not np.allclose(converted_matrix, converted_matrix.T):
|
|
299
|
+
msg = "Matrix is not symmetric, its symmetric part will be considered"
|
|
300
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
301
|
+
|
|
302
|
+
converted_matrix = (converted_matrix + converted_matrix.T) / 2
|
|
303
|
+
|
|
304
|
+
n_vars = len(converted_matrix)
|
|
305
|
+
index = np.arange(n_vars)
|
|
306
|
+
|
|
307
|
+
upper_triangular_mask = index.reshape(-1, 1) < index
|
|
308
|
+
converted_matrix[upper_triangular_mask] *= 2
|
|
309
|
+
np.fill_diagonal(upper_triangular_mask, val=True)
|
|
310
|
+
coefficients = converted_matrix[upper_triangular_mask]
|
|
311
|
+
|
|
312
|
+
exponents = get_quadratic_exponents(n_vars)
|
|
313
|
+
|
|
314
|
+
return cls(exponents, coefficients)
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def zeros(cls, n_vars: int) -> Polynomial:
|
|
318
|
+
"""Create a zero polynomial.
|
|
319
|
+
|
|
320
|
+
Returns a polynomial with a single monomial (the constant 0)
|
|
321
|
+
in `n_vars` variables.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
n_vars : int
|
|
326
|
+
Number of variables in the polynomial.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
Polynomial
|
|
331
|
+
A zero polynomial.
|
|
332
|
+
|
|
333
|
+
Raises
|
|
334
|
+
------
|
|
335
|
+
TypeError
|
|
336
|
+
- If `n_vars` is not an int.
|
|
337
|
+
ValueError
|
|
338
|
+
- If `n_vars` is less than 1.
|
|
339
|
+
|
|
340
|
+
Notes
|
|
341
|
+
-----
|
|
342
|
+
Primarily intended for internal use in specific cases.
|
|
343
|
+
|
|
344
|
+
Examples
|
|
345
|
+
--------
|
|
346
|
+
>>> poly = Polynomial.zeros(3)
|
|
347
|
+
>>> poly
|
|
348
|
+
0
|
|
349
|
+
>>> poly.exponents
|
|
350
|
+
array([[0, 0, 0]])
|
|
351
|
+
>>> poly.coefficients
|
|
352
|
+
array([0.])
|
|
353
|
+
"""
|
|
354
|
+
if not isinstance(n_vars, int):
|
|
355
|
+
msg = f"n_vars must be an int, got {type(n_vars)}."
|
|
356
|
+
raise TypeError(msg)
|
|
357
|
+
|
|
358
|
+
if n_vars < 1:
|
|
359
|
+
msg = f"n_vars must be greater or equal to 1, got {n_vars}"
|
|
360
|
+
raise ValueError(msg)
|
|
361
|
+
|
|
362
|
+
return cls(np.zeros((1, n_vars), dtype=np.int_), np.zeros(1))
|
|
363
|
+
|
|
364
|
+
def prune(self) -> Polynomial:
|
|
365
|
+
"""Prune the empty monomials of a polynomial.
|
|
366
|
+
|
|
367
|
+
Removes all monomials whose associated coefficients are exactly zero.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
Polynomial
|
|
372
|
+
A pruned polynomial, containing only monomials with non-zero coefficients.
|
|
373
|
+
|
|
374
|
+
Notes
|
|
375
|
+
-----
|
|
376
|
+
If all coefficients are zero, a [`zeros`][polyany.Polynomial.zeros] polynomial
|
|
377
|
+
with the same number of variables is returned.
|
|
378
|
+
|
|
379
|
+
Examples
|
|
380
|
+
--------
|
|
381
|
+
>>> poly = Polynomial.univariate([1, 0, 0, 1])
|
|
382
|
+
>>> poly.exponents
|
|
383
|
+
array([[0],
|
|
384
|
+
[1],
|
|
385
|
+
[2],
|
|
386
|
+
[3]])
|
|
387
|
+
|
|
388
|
+
This polynomial has four terms, but only the first and last have a
|
|
389
|
+
non-zero coefficient.
|
|
390
|
+
|
|
391
|
+
>>> pruned = poly.prune()
|
|
392
|
+
>>> pruned.exponents
|
|
393
|
+
array([[0],
|
|
394
|
+
[3]])
|
|
395
|
+
|
|
396
|
+
The result keeps only the non-empty monomials, discarding all others.
|
|
397
|
+
"""
|
|
398
|
+
non_empty_mask = self.coefficients != 0
|
|
399
|
+
|
|
400
|
+
if not np.any(non_empty_mask):
|
|
401
|
+
return self.__class__.zeros(self.n_vars)
|
|
402
|
+
|
|
403
|
+
return self.__class__(
|
|
404
|
+
self.exponents[non_empty_mask], self.coefficients[non_empty_mask]
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def __call__(self, point: ArrayLike) -> np.float64:
|
|
408
|
+
"""Evaluate the polynomial at a given point
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
point : ArrayLike
|
|
413
|
+
A point with `n_vars` components.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
np.float64
|
|
418
|
+
The result of evaluating the polynomial at `point`.
|
|
419
|
+
|
|
420
|
+
Raises
|
|
421
|
+
------
|
|
422
|
+
TypeError
|
|
423
|
+
- If `point` cannot be safely converted to a NumPy 1D-array of floats.
|
|
424
|
+
ValueError
|
|
425
|
+
- If `point` does not have exactly one dimension.
|
|
426
|
+
- If `point` does not have `n_vars` components.
|
|
427
|
+
|
|
428
|
+
Examples
|
|
429
|
+
--------
|
|
430
|
+
For univariate polynomials:
|
|
431
|
+
|
|
432
|
+
>>> poly = Polynomial.univariate([1, 2, 3])
|
|
433
|
+
>>> poly([0])
|
|
434
|
+
np.float64(1.0)
|
|
435
|
+
>>> poly([2])
|
|
436
|
+
np.float64(17.0)
|
|
437
|
+
|
|
438
|
+
For multivariate polynomials:
|
|
439
|
+
>>> exponents = [[0, 0],
|
|
440
|
+
... [1, 0],
|
|
441
|
+
... [0, 1],
|
|
442
|
+
... [1, 1]]
|
|
443
|
+
>>> coefficients = [9, 7, 5, 3]
|
|
444
|
+
>>> poly = Polynomial(exponents, coefficients)
|
|
445
|
+
>>> poly([0, 0])
|
|
446
|
+
np.float64(9.0)
|
|
447
|
+
>>> poly([1, 2])
|
|
448
|
+
np.float64(32.0)
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
converted_point = np.asarray(point).astype(dtype=np.float64, casting="safe")
|
|
452
|
+
except Exception as e:
|
|
453
|
+
msg = (
|
|
454
|
+
"Point must be safe-convertible to NumPy 1D-arrays with float entries."
|
|
455
|
+
)
|
|
456
|
+
raise TypeError(msg) from e
|
|
457
|
+
|
|
458
|
+
if converted_point.ndim != 1:
|
|
459
|
+
msg = f"Point must have 1 dimension, got {converted_point.ndim}."
|
|
460
|
+
raise ValueError(msg)
|
|
461
|
+
|
|
462
|
+
if len(converted_point) != self.n_vars:
|
|
463
|
+
msg = (
|
|
464
|
+
f"Point must have {self.n_vars} component(s), "
|
|
465
|
+
f"got {len(converted_point)}."
|
|
466
|
+
)
|
|
467
|
+
raise ValueError(msg)
|
|
468
|
+
|
|
469
|
+
if np.all(converted_point == 0):
|
|
470
|
+
if np.all(self.exponents[0] == 0):
|
|
471
|
+
return self.coefficients[0]
|
|
472
|
+
return np.float64(0)
|
|
473
|
+
|
|
474
|
+
return self.coefficients @ np.prod(
|
|
475
|
+
np.power(converted_point, self.exponents), axis=1
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def __neg__(self) -> Polynomial:
|
|
479
|
+
"""The negation of the polynomial.
|
|
480
|
+
|
|
481
|
+
All coefficients are multiplied by `-1`. The exponents remain unchanged.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
Polynomial
|
|
486
|
+
A new polynomial with negated coefficients.
|
|
487
|
+
"""
|
|
488
|
+
return self.__class__(self.exponents.copy(), -self.coefficients)
|
|
489
|
+
|
|
490
|
+
def __add__(self, other: object) -> Polynomial:
|
|
491
|
+
"""Addition with another polynomial or scalar
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
other : object
|
|
496
|
+
The value to be added. A scalar can be an int, float, or NumPy scalars.
|
|
497
|
+
|
|
498
|
+
Returns
|
|
499
|
+
-------
|
|
500
|
+
Polynomial
|
|
501
|
+
A new polynomial representing the sum.
|
|
502
|
+
"""
|
|
503
|
+
if not isinstance(other, ALGEBRAIC_TYPE): # pragma: no cover
|
|
504
|
+
return NotImplemented
|
|
505
|
+
|
|
506
|
+
if isinstance(other, SCALAR_TYPE):
|
|
507
|
+
return self._add_scalar(other)
|
|
508
|
+
|
|
509
|
+
return self._add_polynomial(other)
|
|
510
|
+
|
|
511
|
+
def _add_scalar(self, other: Scalar) -> Polynomial:
|
|
512
|
+
coefficients = self.coefficients.copy()
|
|
513
|
+
exponents = self.exponents.copy()
|
|
514
|
+
|
|
515
|
+
has_constant_term = np.all(self.exponents[0] == 0)
|
|
516
|
+
|
|
517
|
+
if has_constant_term:
|
|
518
|
+
coefficients[0] += other
|
|
519
|
+
else:
|
|
520
|
+
exponents = np.vstack(
|
|
521
|
+
(np.zeros((1, self.n_vars), dtype=exponents.dtype), exponents)
|
|
522
|
+
)
|
|
523
|
+
coefficients = np.concatenate((np.atleast_1d(other), coefficients))
|
|
524
|
+
|
|
525
|
+
return self.__class__(exponents, coefficients)
|
|
526
|
+
|
|
527
|
+
def _add_polynomial(self, other: Polynomial) -> Polynomial:
|
|
528
|
+
max_n_vars = max(self.n_vars, other.n_vars)
|
|
529
|
+
|
|
530
|
+
self_exponents = domain_expansion(self.exponents, max_n_vars)
|
|
531
|
+
other_exponents = domain_expansion(other.exponents, max_n_vars)
|
|
532
|
+
|
|
533
|
+
stacked_exponents = np.vstack((self_exponents, other_exponents))
|
|
534
|
+
stacked_coefficients = np.concatenate((self.coefficients, other.coefficients))
|
|
535
|
+
|
|
536
|
+
exponents, indices = np.unique(stacked_exponents, axis=0, return_inverse=True)
|
|
537
|
+
coefficients = np.zeros(len(exponents))
|
|
538
|
+
np.add.at(coefficients, indices, stacked_coefficients)
|
|
539
|
+
|
|
540
|
+
return self.__class__(exponents, coefficients)
|
|
541
|
+
|
|
542
|
+
def __sub__(self, other: Algebraic) -> Polynomial:
|
|
543
|
+
"""Subtraction with another polynomial or scalar
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
other : Algebraic
|
|
548
|
+
The value to be subtracted. A scalar can be an int, float,
|
|
549
|
+
or NumPy scalars.
|
|
550
|
+
|
|
551
|
+
Returns
|
|
552
|
+
-------
|
|
553
|
+
Polynomial
|
|
554
|
+
A new polynomial representing the difference.
|
|
555
|
+
"""
|
|
556
|
+
return self.__add__(-other)
|
|
557
|
+
|
|
558
|
+
def __radd__(self, other: Scalar) -> Polynomial:
|
|
559
|
+
return self.__add__(other)
|
|
560
|
+
|
|
561
|
+
def __rsub__(self, other: Scalar) -> Polynomial:
|
|
562
|
+
return (-self).__add__(other)
|
|
563
|
+
|
|
564
|
+
def __mul__(self, other: object) -> Polynomial:
|
|
565
|
+
"""Multiplication with another polynomial or scalar
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
other : object
|
|
570
|
+
The value to be multiplied. A scalar can be an int, float, or NumPy scalars.
|
|
571
|
+
|
|
572
|
+
Returns
|
|
573
|
+
-------
|
|
574
|
+
Polynomial
|
|
575
|
+
A new polynomial representing the multiplication.
|
|
576
|
+
"""
|
|
577
|
+
if not isinstance(other, ALGEBRAIC_TYPE): # pragma: no cover
|
|
578
|
+
return NotImplemented
|
|
579
|
+
|
|
580
|
+
if isinstance(other, SCALAR_TYPE):
|
|
581
|
+
return self._mul_scalar(other)
|
|
582
|
+
|
|
583
|
+
return self._mul_polynomial(other)
|
|
584
|
+
|
|
585
|
+
def _mul_scalar(self, other: Scalar) -> Polynomial:
|
|
586
|
+
if other == 0:
|
|
587
|
+
return self.__class__.zeros(self.n_vars)
|
|
588
|
+
|
|
589
|
+
coefficients = self.coefficients * other
|
|
590
|
+
|
|
591
|
+
return self.__class__(self.exponents.copy(), coefficients)
|
|
592
|
+
|
|
593
|
+
def _mul_polynomial(self, other: Polynomial) -> Polynomial:
|
|
594
|
+
max_n_vars = max(self.n_vars, other.n_vars)
|
|
595
|
+
|
|
596
|
+
self_exponents = domain_expansion(self.exponents, max_n_vars)
|
|
597
|
+
other_exponents = domain_expansion(other.exponents, max_n_vars)
|
|
598
|
+
|
|
599
|
+
cross_exponents = (
|
|
600
|
+
self_exponents[np.newaxis, :, :] + other_exponents[:, np.newaxis, :]
|
|
601
|
+
).reshape(-1, max_n_vars)
|
|
602
|
+
|
|
603
|
+
cross_coefficients = (
|
|
604
|
+
self.coefficients[np.newaxis, :] * other.coefficients[:, np.newaxis]
|
|
605
|
+
).ravel()
|
|
606
|
+
|
|
607
|
+
exponents, indices = np.unique(cross_exponents, axis=0, return_inverse=True)
|
|
608
|
+
coefficients = np.zeros(len(exponents))
|
|
609
|
+
np.add.at(coefficients, indices, cross_coefficients)
|
|
610
|
+
|
|
611
|
+
return self.__class__(exponents, coefficients)
|
|
612
|
+
|
|
613
|
+
@np.errstate(divide="raise")
|
|
614
|
+
def __truediv__(self, other: Scalar) -> Polynomial:
|
|
615
|
+
"""Division with a scalar
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
other : Scalar
|
|
620
|
+
The value to divide the polynomial by.
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
Polynomial
|
|
625
|
+
A new polynomial representing the division.
|
|
626
|
+
|
|
627
|
+
Raises
|
|
628
|
+
------
|
|
629
|
+
ZeroDivisionError
|
|
630
|
+
- If `other` is a builtin scalar and equal to zero.
|
|
631
|
+
FloatingPointError
|
|
632
|
+
- If `other` is a NumPy scalar and equal to zero.
|
|
633
|
+
|
|
634
|
+
Notes
|
|
635
|
+
-----
|
|
636
|
+
Currently, division can only be performed between polynomials and scalars.
|
|
637
|
+
"""
|
|
638
|
+
if not isinstance(other, SCALAR_TYPE): # pragma: no cover
|
|
639
|
+
return NotImplemented
|
|
640
|
+
|
|
641
|
+
return self.__mul__(1 / other)
|
|
642
|
+
|
|
643
|
+
def __rmul__(self, other: Scalar) -> Polynomial:
|
|
644
|
+
return self.__mul__(other)
|
|
645
|
+
|
|
646
|
+
def __eq__(self, other: object) -> bool:
|
|
647
|
+
if not isinstance(other, self.__class__):
|
|
648
|
+
return NotImplemented
|
|
649
|
+
|
|
650
|
+
if self.degree != other.degree or self.n_vars != other.n_vars:
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
return np.allclose(self.coefficients, other.coefficients)
|
|
654
|
+
|
|
655
|
+
def __lt__(self, other: object) -> bool:
|
|
656
|
+
return NotImplemented
|
|
657
|
+
|
|
658
|
+
def __le__(self, other: object) -> bool:
|
|
659
|
+
return NotImplemented
|
|
660
|
+
|
|
661
|
+
def __gt__(self, other: object) -> bool:
|
|
662
|
+
return NotImplemented
|
|
663
|
+
|
|
664
|
+
def __ge__(self, other: object) -> bool:
|
|
665
|
+
return NotImplemented
|
|
666
|
+
|
|
667
|
+
def shift(self, k: int = 1) -> Polynomial:
|
|
668
|
+
"""Shifts the polynomial variables.
|
|
669
|
+
|
|
670
|
+
This method returns a new polynomial with its variables shifted.
|
|
671
|
+
A positive shift adds extra variables (increasing all variable indices).
|
|
672
|
+
A negative shift removes variables, but only if they are empty.
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
Parameters
|
|
676
|
+
----------
|
|
677
|
+
k : int, optional
|
|
678
|
+
The shift count. If positive, adds `k` extra variables
|
|
679
|
+
(increase the variable indices). If negative, remove the first `abs(k)`
|
|
680
|
+
variables, but only if they are empty (all corresponding exponents
|
|
681
|
+
are zero).
|
|
682
|
+
|
|
683
|
+
Returns
|
|
684
|
+
-------
|
|
685
|
+
Polynomial
|
|
686
|
+
A new polynomial with shifted variables.
|
|
687
|
+
|
|
688
|
+
Raises
|
|
689
|
+
------
|
|
690
|
+
TypeError
|
|
691
|
+
- If `k` is not an int.
|
|
692
|
+
ValueError
|
|
693
|
+
- If `k` is negative and the number of variables after shifting
|
|
694
|
+
would be less than one.
|
|
695
|
+
- If any of the first `abs(k)` variables are
|
|
696
|
+
not empty.
|
|
697
|
+
|
|
698
|
+
Notes
|
|
699
|
+
-----
|
|
700
|
+
If `k` = 0 a copy of the polynomial is returned.
|
|
701
|
+
|
|
702
|
+
The Python shift operators can be used as a syntactic sugar for this method.
|
|
703
|
+
`poly >> 3` is equivalent to `poly.shift(3)`, and `poly << 2` is equivalent to
|
|
704
|
+
`poly.shift(-2)`.
|
|
705
|
+
|
|
706
|
+
This method is reversible as long as both directions are valid.
|
|
707
|
+
|
|
708
|
+
- The statement `poly.shift(k).shift(-k)` will return a polynomial equal to the
|
|
709
|
+
original object `poly`.
|
|
710
|
+
|
|
711
|
+
- Likewise, if `poly.shift(-k)` is possible, then applying `shift(k)` after it
|
|
712
|
+
will also return a copy of `poly`.
|
|
713
|
+
|
|
714
|
+
Examples
|
|
715
|
+
--------
|
|
716
|
+
Adding extra variables (shift right), increases the variable indices.
|
|
717
|
+
|
|
718
|
+
>>> poly = Polynomial.univariate([1, 2, 3])
|
|
719
|
+
>>> poly
|
|
720
|
+
1 + 2*x_1 + 3*x_1^2
|
|
721
|
+
>>> poly.shift(2)
|
|
722
|
+
1 + 2*x_3 + 3*x_3^2
|
|
723
|
+
>>> poly >> 2 # equivalent syntax
|
|
724
|
+
1 + 2*x_3 + 3*x_3^2
|
|
725
|
+
|
|
726
|
+
Removing empty variables (shift left), decreases the variable indices.
|
|
727
|
+
|
|
728
|
+
>>> poly = Polynomial([[0, 1], [0, 3], [0, 5]], [10, 20, 30])
|
|
729
|
+
>>> poly
|
|
730
|
+
10*x_2 + 20*x_2^3 + 30*x_2^5
|
|
731
|
+
>>> poly.shift(-1)
|
|
732
|
+
10*x_1 + 20*x_1^3 + 30*x_1^5
|
|
733
|
+
>>> poly << 1 # equivalent syntax
|
|
734
|
+
10*x_1 + 20*x_1^3 + 30*x_1^5
|
|
735
|
+
"""
|
|
736
|
+
if not isinstance(k, int):
|
|
737
|
+
msg = f"k must be an int, got {type(k)}."
|
|
738
|
+
raise TypeError(msg)
|
|
739
|
+
|
|
740
|
+
exponents = self.exponents.copy()
|
|
741
|
+
coefficients = self.coefficients.copy()
|
|
742
|
+
|
|
743
|
+
if k < 0:
|
|
744
|
+
vars_to_remove = ", ".join(["x_" + str(idx + 1) for idx in range(abs(k))])
|
|
745
|
+
|
|
746
|
+
if self.n_vars + k < 1:
|
|
747
|
+
msg = (
|
|
748
|
+
f"Cannot remove ({vars_to_remove}), "
|
|
749
|
+
"at least one variable must remain, "
|
|
750
|
+
f"ensure that k >= {-self.n_vars + 1}."
|
|
751
|
+
)
|
|
752
|
+
raise ValueError(msg)
|
|
753
|
+
|
|
754
|
+
rows_with_nonzero_exponents = np.any(exponents[:, : abs(k)] != 0, axis=1)
|
|
755
|
+
has_nonzero_coefficients = bool(
|
|
756
|
+
np.any(coefficients[rows_with_nonzero_exponents] != 0)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
if has_nonzero_coefficients:
|
|
760
|
+
msg = (
|
|
761
|
+
f"Cannot remove ({vars_to_remove}), "
|
|
762
|
+
"at least one associated coefficient is not zero."
|
|
763
|
+
)
|
|
764
|
+
raise ValueError(msg)
|
|
765
|
+
|
|
766
|
+
exponents = exponents[~rows_with_nonzero_exponents, abs(k) :]
|
|
767
|
+
coefficients = coefficients[~rows_with_nonzero_exponents]
|
|
768
|
+
|
|
769
|
+
if k > 0:
|
|
770
|
+
exponents = np.hstack(
|
|
771
|
+
(
|
|
772
|
+
np.zeros(shape=(len(exponents), k), dtype=np.int_),
|
|
773
|
+
exponents,
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
return self.__class__(exponents, coefficients)
|
|
778
|
+
|
|
779
|
+
def __rshift__(self, other: int) -> Polynomial:
|
|
780
|
+
"""Adds extra variables to the Polynomial.
|
|
781
|
+
|
|
782
|
+
A shorthand for `Polynomial.shift(k)` with `k > 0` using the right shift
|
|
783
|
+
operator (`>>`). For more details, see the
|
|
784
|
+
[`Polynomial.shift()`][polyany.polynomial.Polynomial.shift] method.
|
|
785
|
+
|
|
786
|
+
Parameters
|
|
787
|
+
----------
|
|
788
|
+
other : int
|
|
789
|
+
The shift count. Must be a non-negative integer.
|
|
790
|
+
|
|
791
|
+
Returns
|
|
792
|
+
-------
|
|
793
|
+
Polynomial
|
|
794
|
+
A new polynomial with shifted variables.
|
|
795
|
+
|
|
796
|
+
Raises
|
|
797
|
+
------
|
|
798
|
+
ValueError
|
|
799
|
+
- If the shift count (`other`) is negative.
|
|
800
|
+
"""
|
|
801
|
+
if not isinstance(other, int): # pragma: no cover
|
|
802
|
+
return NotImplemented
|
|
803
|
+
|
|
804
|
+
if other < 0:
|
|
805
|
+
msg = "Shift count must be non-negative."
|
|
806
|
+
raise ValueError(msg)
|
|
807
|
+
|
|
808
|
+
return self.shift(other)
|
|
809
|
+
|
|
810
|
+
def __lshift__(self, other: int) -> Polynomial:
|
|
811
|
+
"""Removes empty variables of the Polynomial.
|
|
812
|
+
|
|
813
|
+
A shorthand for `Polynomial.shift(k)` with `k < 0` using the left shift
|
|
814
|
+
operator (`<<`). For more details, see the
|
|
815
|
+
[`Polynomial.shift()`][polyany.polynomial.Polynomial.shift] method.
|
|
816
|
+
|
|
817
|
+
Parameters
|
|
818
|
+
----------
|
|
819
|
+
other : int
|
|
820
|
+
The shift count. Must be a non-negative integer.
|
|
821
|
+
|
|
822
|
+
Returns
|
|
823
|
+
-------
|
|
824
|
+
Polynomial
|
|
825
|
+
A new polynomial with shifted variables.
|
|
826
|
+
|
|
827
|
+
Raises
|
|
828
|
+
------
|
|
829
|
+
ValueError
|
|
830
|
+
- If the shift count (`other`) is negative.
|
|
831
|
+
"""
|
|
832
|
+
if not isinstance(other, int): # pragma: no cover
|
|
833
|
+
return NotImplemented
|
|
834
|
+
|
|
835
|
+
if other < 0:
|
|
836
|
+
msg = "Shift count must be non-negative."
|
|
837
|
+
raise ValueError(msg)
|
|
838
|
+
|
|
839
|
+
return self.shift(-other)
|
|
840
|
+
|
|
841
|
+
def partial(self, var_index: int) -> Polynomial:
|
|
842
|
+
"""Partial derivative of a polynomial
|
|
843
|
+
|
|
844
|
+
Computes the partial derivative of the polynomial with respect to the variable
|
|
845
|
+
indexed by `var_index`.
|
|
846
|
+
|
|
847
|
+
Parameters
|
|
848
|
+
----------
|
|
849
|
+
var_index : int
|
|
850
|
+
The variable index to perform the partial derivative (zero-based).
|
|
851
|
+
|
|
852
|
+
Returns
|
|
853
|
+
-------
|
|
854
|
+
Polynomial
|
|
855
|
+
The resulting polynomial after differentiation.
|
|
856
|
+
|
|
857
|
+
Raises
|
|
858
|
+
------
|
|
859
|
+
TypeError
|
|
860
|
+
- If `var_index` is not an int.
|
|
861
|
+
ValueError
|
|
862
|
+
- If `var_index` is outside the valid range [0, `n_vars` - 1].
|
|
863
|
+
|
|
864
|
+
Examples
|
|
865
|
+
--------
|
|
866
|
+
>>> poly = Polynomial([[1, 0], [2, 1]], [3, 5])
|
|
867
|
+
>>> poly
|
|
868
|
+
3*x_1 + 5*x_1^2*x_2
|
|
869
|
+
>>> poly.partial(0)
|
|
870
|
+
3 + 10*x_1*x_2
|
|
871
|
+
>>> poly.partial(1)
|
|
872
|
+
5*x_1^2
|
|
873
|
+
"""
|
|
874
|
+
if not isinstance(var_index, int):
|
|
875
|
+
msg = f"var_index must be an int, got {type(var_index)}."
|
|
876
|
+
raise TypeError(msg)
|
|
877
|
+
|
|
878
|
+
if not (0 <= var_index < self.n_vars):
|
|
879
|
+
if self.n_vars == 1:
|
|
880
|
+
msg = "For a univariate polynomial, var_index must be 0"
|
|
881
|
+
else: # pragma: no cover
|
|
882
|
+
msg = f"var_index must be between 0 and {self.n_vars - 1} (inclusive)"
|
|
883
|
+
msg += f", got {var_index}."
|
|
884
|
+
raise ValueError(msg)
|
|
885
|
+
|
|
886
|
+
exponents = self.exponents.copy()
|
|
887
|
+
coefficients = self.coefficients.copy()
|
|
888
|
+
|
|
889
|
+
coefficients *= exponents[:, var_index]
|
|
890
|
+
exponents[:, var_index] = np.maximum(0, exponents[:, var_index] - 1)
|
|
891
|
+
|
|
892
|
+
non_empty_mask = coefficients != 0
|
|
893
|
+
|
|
894
|
+
if not np.any(non_empty_mask):
|
|
895
|
+
return self.__class__.zeros(self.n_vars)
|
|
896
|
+
|
|
897
|
+
exponents = exponents[non_empty_mask]
|
|
898
|
+
coefficients = coefficients[non_empty_mask]
|
|
899
|
+
|
|
900
|
+
return self.__class__(exponents, coefficients)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
SCALAR_TYPE = (int, float, np.integer, np.floating)
|
|
904
|
+
ALGEBRAIC_TYPE = (*SCALAR_TYPE, Polynomial)
|
polyany/py.typed
ADDED
|
File without changes
|
polyany/types.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import TypeAlias
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .polynomial import Polynomial
|
|
6
|
+
|
|
7
|
+
Scalar: TypeAlias = int | float | np.integer | np.floating
|
|
8
|
+
"""A numeric scalar that can be a builtin numeric type or a NumPy scalar."""
|
|
9
|
+
Algebraic: TypeAlias = Scalar | Polynomial
|
|
10
|
+
"""An algebraic element that can be a scalar or a Polynomial."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyany
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A Python package for algebraic manipulation of multivariate polynomials
|
|
5
|
+
Project-URL: Homepage, https://rolmip.github.io/polyany/
|
|
6
|
+
Project-URL: Documentation, https://rolmip.github.io/polyany/
|
|
7
|
+
Project-URL: Repository, https://github.com/rolmip/polyany
|
|
8
|
+
Project-URL: Issues, https://github.com/rolmip/polyany/issues
|
|
9
|
+
Author-email: Cristiano Agulhari <agulhari@utfpr.edu.br>, Esdras Battosti <esdras.2019@alunos.utfpr.edu.br>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: algebra,multivariate,polynomials
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: Unix
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: numpy>=2.3.1
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<h1 align="center">
|
|
26
|
+
<img src="docs/assets/polyany_logo.png" alt="PolyAny Logo" height="200">
|
|
27
|
+
</h1><br>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="https://img.shields.io/badge/status-pre--alpha-orange" alt="Static Badge">
|
|
31
|
+
<a href="https://codecov.io/gh/rolmip/polyany">
|
|
32
|
+
<img src="https://codecov.io/gh/rolmip/polyany/graph/badge.svg?token=XMNXDY6AZ7" alt="codecov">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://github.com/rolmip/polyany/actions/workflows/tests.yml">
|
|
35
|
+
<img src="https://github.com/rolmip/polyany/actions/workflows/tests.yml/badge.svg" alt="Tests">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://github.com/pre-commit/pre-commit">
|
|
38
|
+
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit" alt="pre-commit">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://results.pre-commit.ci/latest/github/rolmip/polyany/main">
|
|
41
|
+
<img src="https://results.pre-commit.ci/badge/github/rolmip/polyany/main.svg" alt="pre-commit.ci status">
|
|
42
|
+
</a>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<strong>A Python package for algebraic manipulation of multivariate polynomials.</strong>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
> ๐ง **This package is under active development.**
|
|
49
|
+
>
|
|
50
|
+
> It is not yet stable or ready for production use. **Expect breaking changes!**
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## โจ Overview
|
|
55
|
+
|
|
56
|
+
PolyAny provides a flexible framework for representing and manipulating multivariate polynomials using structured, non-symbolic representations.
|
|
57
|
+
|
|
58
|
+
Unlike symbolic engines, PolyAny operates directly on the algebraic structure of polynomials (coefficients and exponents), enabling integration with numerical libraries and efficient structural transformations.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ๐ง Features (planned)
|
|
63
|
+
|
|
64
|
+
- Polynomial creation from multiple formats (list, tuples, NumPy arrays, quadratic forms, ...)
|
|
65
|
+
- Support for multivariate expressions
|
|
66
|
+
- Algebraic operations: addition, multiplication, truncation, homogenization, ...
|
|
67
|
+
- Polynomial exporting into LaTeX code
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ๐ฆ Installation
|
|
72
|
+
|
|
73
|
+
> โ ๏ธ Not yet available in PyPI.
|
|
74
|
+
|
|
75
|
+
For local development, see the [instructions in the documentation](https://rolmip.github.io/polyany/installation/#how-to-contribute)
|
|
76
|
+
|
|
77
|
+
## ๐ License
|
|
78
|
+
|
|
79
|
+
This project is open-source and licensed under the BSD-3-Clause.
|
|
80
|
+
|
|
81
|
+
## ๐ฅ Contributors
|
|
82
|
+
|
|
83
|
+
PolyAny is maintained by the **ROLMIP** developers:
|
|
84
|
+
|
|
85
|
+
* [Cristiano Agulhari](mailto:agulhari@utfpr.edu.br)
|
|
86
|
+
* [Esdras Battosti](mailto:esdras.2019@alunos.utfpr.edu.br)
|
|
87
|
+
|
|
88
|
+
## ๐งช Status
|
|
89
|
+
|
|
90
|
+
This repository is part of the early foundation of **RolmiPy**, a Python implementation of ROLMIP.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
polyany/__init__.py,sha256=HjyYmY7o77lPEucg6FXExFGudr7SnnrG5fFgjw9feJo,61
|
|
2
|
+
polyany/exponents.py,sha256=cKvuVuSZY_KvdhiLO3_ibvbErTZlWaKTgGM0xEpngPk,571
|
|
3
|
+
polyany/polynomial.py,sha256=kVKz9I7hhIUKgqegqO198j6V14wq3KWG1ZuJ7dOevoE,28641
|
|
4
|
+
polyany/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
polyany/types.py,sha256=RkWmTrYVatra3y6vpVrQpFXa8KVswbZgorPmgdq0RNI,330
|
|
6
|
+
polyany-0.3.0.dist-info/METADATA,sha256=lVnSBf5LkzJKkIphIEA5CppfNbDashLIh_V223lP1qI,3469
|
|
7
|
+
polyany-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
polyany-0.3.0.dist-info/licenses/LICENSE,sha256=pOEQ_RLX-UWgvEY0jy9iHRDt2crd-owHkD8vB8iL-Fk,1541
|
|
9
|
+
polyany-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Cristiano Marcos Agulhari and Esdras Battosti da Silva
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|