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 ADDED
@@ -0,0 +1,3 @@
1
+ from .polynomial import Polynomial
2
+
3
+ __all__ = ["Polynomial"]
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.