homlab-polynomial 0.1.0__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.
- homlab_polynomial-0.1.0/LICENSE +21 -0
- homlab_polynomial-0.1.0/PKG-INFO +82 -0
- homlab_polynomial-0.1.0/README.md +70 -0
- homlab_polynomial-0.1.0/pyproject.toml +20 -0
- homlab_polynomial-0.1.0/setup.cfg +4 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial/__init__.py +17 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial/core.py +515 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial.egg-info/PKG-INFO +82 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial.egg-info/SOURCES.txt +11 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial.egg-info/dependency_links.txt +1 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial.egg-info/requires.txt +1 -0
- homlab_polynomial-0.1.0/src/homlab_polynomial.egg-info/top_level.txt +1 -0
- homlab_polynomial-0.1.0/tests/test_core.py +103 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Homology Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: homlab-polynomial
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Collect indexed C terms in polynomials with SymPy.
|
|
5
|
+
Author: GGN_2015
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: sympy>=1.10
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# homlab_polynomial
|
|
14
|
+
|
|
15
|
+
`homlab_polynomial` 提供一个 Python 编程入口,用 SymPy 将包含普通变量 `A` 和三下标变量
|
|
16
|
+
`C[x, y, z]` 的字符串表达式整理成按不同 `C[x, y, z]` 分组的形式。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 使用
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from homlab_polynomial import organize_polynomial
|
|
28
|
+
|
|
29
|
+
expr = "A*C[1, 2, 3] + 2*C[1,2,3] + A^2*C[0,0,0]"
|
|
30
|
+
print(organize_polynomial(expr))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
输出:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
C[0, 0, 0]*(A^2) + C[1, 2, 3]*(A + 2)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
负指数也支持:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
organize_polynomial("A^-10*C[0,0,0] + 2*C[0,0,0]/A")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
输出:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
C[0, 0, 0]*(2*A^(-1) + A^(-10))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
说明:
|
|
52
|
+
|
|
53
|
+
- `C[x, y, z]` 的三个下标必须是整数,可以为负数。
|
|
54
|
+
- 输入支持用 `^` 表示指数运算,也支持 Python/SymPy 风格的 `**`。
|
|
55
|
+
- 默认输出也使用 `^`。如果需要 `**`,可以传入 `output_power_operator="**"`。
|
|
56
|
+
- 系数支持有限 Laurent 多项式,因此 `A^-1`、`1/A`、`A^-10` 都可以使用。
|
|
57
|
+
- `C[x, y, z]` 项不会带外部负号;负号会保留在括号中的系数多项式里。
|
|
58
|
+
- 不含 `C` 的纯 `A` 项会作为最后的余项保留。
|
|
59
|
+
|
|
60
|
+
如果需要继续做 SymPy 计算,可以使用:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from homlab_polynomial import organize_polynomial_expr
|
|
64
|
+
|
|
65
|
+
sympy_expr = organize_polynomial_expr("A*C[1,2,3] + C[1,2,3]")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 验算
|
|
69
|
+
|
|
70
|
+
`organize_polynomial()` 默认会在每次整理后进行随机代入验算。它会给 `A` 和所有出现过的
|
|
71
|
+
`C[x, y, z]` 赋非零有理数,检查整理前后的表达式是否一致。
|
|
72
|
+
|
|
73
|
+
也可以直接调用验算函数:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from homlab_polynomial import verify_simplification
|
|
77
|
+
|
|
78
|
+
verify_simplification(
|
|
79
|
+
"A*C[1,2,3] + C[1,2,3]",
|
|
80
|
+
"C[1, 2, 3]*(A + 1)",
|
|
81
|
+
)
|
|
82
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# homlab_polynomial
|
|
2
|
+
|
|
3
|
+
`homlab_polynomial` 提供一个 Python 编程入口,用 SymPy 将包含普通变量 `A` 和三下标变量
|
|
4
|
+
`C[x, y, z]` 的字符串表达式整理成按不同 `C[x, y, z]` 分组的形式。
|
|
5
|
+
|
|
6
|
+
## 安装
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 使用
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from homlab_polynomial import organize_polynomial
|
|
16
|
+
|
|
17
|
+
expr = "A*C[1, 2, 3] + 2*C[1,2,3] + A^2*C[0,0,0]"
|
|
18
|
+
print(organize_polynomial(expr))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
输出:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
C[0, 0, 0]*(A^2) + C[1, 2, 3]*(A + 2)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
负指数也支持:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
organize_polynomial("A^-10*C[0,0,0] + 2*C[0,0,0]/A")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
输出:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
C[0, 0, 0]*(2*A^(-1) + A^(-10))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
说明:
|
|
40
|
+
|
|
41
|
+
- `C[x, y, z]` 的三个下标必须是整数,可以为负数。
|
|
42
|
+
- 输入支持用 `^` 表示指数运算,也支持 Python/SymPy 风格的 `**`。
|
|
43
|
+
- 默认输出也使用 `^`。如果需要 `**`,可以传入 `output_power_operator="**"`。
|
|
44
|
+
- 系数支持有限 Laurent 多项式,因此 `A^-1`、`1/A`、`A^-10` 都可以使用。
|
|
45
|
+
- `C[x, y, z]` 项不会带外部负号;负号会保留在括号中的系数多项式里。
|
|
46
|
+
- 不含 `C` 的纯 `A` 项会作为最后的余项保留。
|
|
47
|
+
|
|
48
|
+
如果需要继续做 SymPy 计算,可以使用:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from homlab_polynomial import organize_polynomial_expr
|
|
52
|
+
|
|
53
|
+
sympy_expr = organize_polynomial_expr("A*C[1,2,3] + C[1,2,3]")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 验算
|
|
57
|
+
|
|
58
|
+
`organize_polynomial()` 默认会在每次整理后进行随机代入验算。它会给 `A` 和所有出现过的
|
|
59
|
+
`C[x, y, z]` 赋非零有理数,检查整理前后的表达式是否一致。
|
|
60
|
+
|
|
61
|
+
也可以直接调用验算函数:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from homlab_polynomial import verify_simplification
|
|
65
|
+
|
|
66
|
+
verify_simplification(
|
|
67
|
+
"A*C[1,2,3] + C[1,2,3]",
|
|
68
|
+
"C[1, 2, 3]*(A + 1)",
|
|
69
|
+
)
|
|
70
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "homlab-polynomial"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Collect indexed C terms in polynomials with SymPy."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "GGN_2015" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"sympy>=1.10"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Public API for homlab_polynomial."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
PolynomialParseError,
|
|
5
|
+
PolynomialVerificationError,
|
|
6
|
+
organize_polynomial,
|
|
7
|
+
organize_polynomial_expr,
|
|
8
|
+
verify_simplification,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PolynomialParseError",
|
|
13
|
+
"PolynomialVerificationError",
|
|
14
|
+
"organize_polynomial",
|
|
15
|
+
"organize_polynomial_expr",
|
|
16
|
+
"verify_simplification",
|
|
17
|
+
]
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Polynomial collection utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from random import Random
|
|
7
|
+
import re
|
|
8
|
+
from typing import Literal, Optional
|
|
9
|
+
|
|
10
|
+
import sympy as sp
|
|
11
|
+
from sympy.parsing.sympy_parser import (
|
|
12
|
+
convert_xor,
|
|
13
|
+
implicit_multiplication_application,
|
|
14
|
+
parse_expr,
|
|
15
|
+
standard_transformations,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Index = tuple[int, int, int]
|
|
20
|
+
PowerOperator = Literal["^", "**"]
|
|
21
|
+
|
|
22
|
+
_C_PATTERN = re.compile(
|
|
23
|
+
r"(?<![A-Za-z_])C\s*\[\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*\]"
|
|
24
|
+
)
|
|
25
|
+
_TOKEN_PREFIX = "_homlab_C_"
|
|
26
|
+
_TRANSFORMATIONS = standard_transformations + (
|
|
27
|
+
convert_xor,
|
|
28
|
+
implicit_multiplication_application,
|
|
29
|
+
)
|
|
30
|
+
_SAFE_GLOBALS = {
|
|
31
|
+
"Add": sp.Add,
|
|
32
|
+
"Float": sp.Float,
|
|
33
|
+
"Integer": sp.Integer,
|
|
34
|
+
"Mul": sp.Mul,
|
|
35
|
+
"Pow": sp.Pow,
|
|
36
|
+
"Rational": sp.Rational,
|
|
37
|
+
"Symbol": sp.Symbol,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PolynomialParseError(ValueError):
|
|
42
|
+
"""Raised when a polynomial string cannot be parsed or collected."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PolynomialVerificationError(ValueError):
|
|
46
|
+
"""Raised when randomized verification finds a mismatch."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class _ParsedPolynomial:
|
|
51
|
+
expr: sp.Expr
|
|
52
|
+
a_symbol: sp.Symbol
|
|
53
|
+
c_symbols: dict[sp.Symbol, Index]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class _CollectedTerm:
|
|
58
|
+
c_powers: tuple[tuple[Index, int], ...]
|
|
59
|
+
coefficient: sp.Expr
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def organize_polynomial(
|
|
63
|
+
expression: str,
|
|
64
|
+
*,
|
|
65
|
+
output_power_operator: PowerOperator = "^",
|
|
66
|
+
verify: bool = True,
|
|
67
|
+
verification_trials: int = 5,
|
|
68
|
+
random_seed: Optional[int] = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Collect a polynomial by indexed ``C[x, y, z]`` terms.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
expression:
|
|
75
|
+
Polynomial string using ``A`` and indexed ``C[x, y, z]`` variables.
|
|
76
|
+
The indices must be integers. Both ``^`` and ``**`` are accepted for
|
|
77
|
+
powers.
|
|
78
|
+
output_power_operator:
|
|
79
|
+
Power operator used in the returned string. The default is ``^`` to
|
|
80
|
+
match the input convention.
|
|
81
|
+
verify:
|
|
82
|
+
Whether to run randomized substitution checks before returning.
|
|
83
|
+
verification_trials:
|
|
84
|
+
Number of randomized substitution checks to run when ``verify`` is
|
|
85
|
+
true.
|
|
86
|
+
random_seed:
|
|
87
|
+
Optional seed for reproducible verification assignments.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
str
|
|
92
|
+
A string in the form ``C[x, y, z]*(polynomial in A) + ...``. If the
|
|
93
|
+
expression contains terms without any ``C`` factor, they are kept as a
|
|
94
|
+
final polynomial in ``A``.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
if output_power_operator not in {"^", "**"}:
|
|
98
|
+
raise ValueError("output_power_operator must be '^' or '**'.")
|
|
99
|
+
|
|
100
|
+
parsed = _parse_polynomial(expression)
|
|
101
|
+
terms = _collect_terms(parsed)
|
|
102
|
+
result = _format_terms(terms, output_power_operator)
|
|
103
|
+
if verify:
|
|
104
|
+
verify_simplification(
|
|
105
|
+
expression,
|
|
106
|
+
result,
|
|
107
|
+
trials=verification_trials,
|
|
108
|
+
random_seed=random_seed,
|
|
109
|
+
)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def organize_polynomial_expr(
|
|
114
|
+
expression: str,
|
|
115
|
+
*,
|
|
116
|
+
verify: bool = True,
|
|
117
|
+
verification_trials: int = 5,
|
|
118
|
+
random_seed: Optional[int] = None,
|
|
119
|
+
) -> sp.Expr:
|
|
120
|
+
"""Return the collected result as a SymPy expression.
|
|
121
|
+
|
|
122
|
+
This is useful when callers want to continue symbolic computation after
|
|
123
|
+
collection. The string API is usually more convenient for display because
|
|
124
|
+
it preserves the ``C[x, y, z]`` notation.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
parsed = _parse_polynomial(expression)
|
|
128
|
+
terms = _collect_terms(parsed)
|
|
129
|
+
result = _terms_to_expr(terms)
|
|
130
|
+
if verify:
|
|
131
|
+
_verify_parsed_against_expr(
|
|
132
|
+
parsed,
|
|
133
|
+
result,
|
|
134
|
+
trials=verification_trials,
|
|
135
|
+
random_seed=random_seed,
|
|
136
|
+
)
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def verify_simplification(
|
|
141
|
+
original_expression: str,
|
|
142
|
+
simplified_expression: str,
|
|
143
|
+
*,
|
|
144
|
+
trials: int = 5,
|
|
145
|
+
random_seed: Optional[int] = None,
|
|
146
|
+
) -> bool:
|
|
147
|
+
"""Verify two expressions by randomized exact substitutions.
|
|
148
|
+
|
|
149
|
+
The function assigns the same random non-zero rational values to ``A`` and
|
|
150
|
+
every indexed ``C[x, y, z]`` appearing on either side, then checks that the
|
|
151
|
+
two expressions evaluate to the same value. It returns ``True`` when all
|
|
152
|
+
trials pass and raises ``PolynomialVerificationError`` on the first
|
|
153
|
+
mismatch.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
original = _parse_polynomial(original_expression)
|
|
157
|
+
simplified = _parse_polynomial(simplified_expression)
|
|
158
|
+
_verify_parsed_pair(
|
|
159
|
+
original,
|
|
160
|
+
simplified,
|
|
161
|
+
trials=trials,
|
|
162
|
+
random_seed=random_seed,
|
|
163
|
+
)
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _terms_to_expr(terms: list[_CollectedTerm]) -> sp.Expr:
|
|
168
|
+
c_base = sp.IndexedBase("C")
|
|
169
|
+
expr_terms: list[sp.Expr] = []
|
|
170
|
+
for term in terms:
|
|
171
|
+
if not term.c_powers:
|
|
172
|
+
expr_terms.append(term.coefficient)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
c_factor = _c_factor_expr(term.c_powers, c_base)
|
|
176
|
+
expr_terms.append(sp.Mul(c_factor, term.coefficient, evaluate=False))
|
|
177
|
+
|
|
178
|
+
if not expr_terms:
|
|
179
|
+
return sp.Integer(0)
|
|
180
|
+
return sp.Add(*expr_terms, evaluate=False)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _verify_parsed_against_expr(
|
|
184
|
+
original: _ParsedPolynomial,
|
|
185
|
+
simplified_expr: sp.Expr,
|
|
186
|
+
*,
|
|
187
|
+
trials: int,
|
|
188
|
+
random_seed: Optional[int],
|
|
189
|
+
) -> None:
|
|
190
|
+
simplified = _parse_polynomial(_sympy_expr_to_c_string(simplified_expr))
|
|
191
|
+
_verify_parsed_pair(
|
|
192
|
+
original,
|
|
193
|
+
simplified,
|
|
194
|
+
trials=trials,
|
|
195
|
+
random_seed=random_seed,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _verify_parsed_pair(
|
|
200
|
+
original: _ParsedPolynomial,
|
|
201
|
+
simplified: _ParsedPolynomial,
|
|
202
|
+
*,
|
|
203
|
+
trials: int,
|
|
204
|
+
random_seed: Optional[int],
|
|
205
|
+
) -> None:
|
|
206
|
+
if not isinstance(trials, int) or trials < 1:
|
|
207
|
+
raise ValueError("trials must be a positive integer.")
|
|
208
|
+
|
|
209
|
+
rng = Random(random_seed)
|
|
210
|
+
c_indices = set(original.c_symbols.values()) | set(simplified.c_symbols.values())
|
|
211
|
+
|
|
212
|
+
for trial in range(1, trials + 1):
|
|
213
|
+
a_value = _random_nonzero_rational(rng)
|
|
214
|
+
c_values = {
|
|
215
|
+
index: _random_nonzero_rational(rng)
|
|
216
|
+
for index in c_indices
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
original_value = _evaluate_parsed(original, a_value, c_values)
|
|
220
|
+
simplified_value = _evaluate_parsed(simplified, a_value, c_values)
|
|
221
|
+
difference = sp.simplify(original_value - simplified_value)
|
|
222
|
+
if _is_zero(difference):
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
assignments = _format_assignments(a_value, c_values)
|
|
226
|
+
raise PolynomialVerificationError(
|
|
227
|
+
"verification failed on trial "
|
|
228
|
+
f"{trial}: original={original_value}, simplified={simplified_value}, "
|
|
229
|
+
f"assignments={assignments}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _evaluate_parsed(
|
|
234
|
+
parsed: _ParsedPolynomial,
|
|
235
|
+
a_value: sp.Rational,
|
|
236
|
+
c_values: dict[Index, sp.Rational],
|
|
237
|
+
) -> sp.Expr:
|
|
238
|
+
substitutions: dict[sp.Symbol, sp.Rational] = {parsed.a_symbol: a_value}
|
|
239
|
+
substitutions.update(
|
|
240
|
+
{
|
|
241
|
+
symbol: c_values[index]
|
|
242
|
+
for symbol, index in parsed.c_symbols.items()
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
return sp.cancel(parsed.expr.subs(substitutions))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _random_nonzero_rational(rng: Random) -> sp.Rational:
|
|
249
|
+
numerator = rng.choice([-9, -8, -7, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 7, 8, 9])
|
|
250
|
+
denominator = rng.choice([1, 2, 3, 5, 7])
|
|
251
|
+
return sp.Rational(numerator, denominator)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _is_zero(value: sp.Expr) -> bool:
|
|
255
|
+
if value == 0:
|
|
256
|
+
return True
|
|
257
|
+
if value.is_number:
|
|
258
|
+
return abs(complex(sp.N(value))) < 1e-10
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _format_assignments(
|
|
263
|
+
a_value: sp.Rational,
|
|
264
|
+
c_values: dict[Index, sp.Rational],
|
|
265
|
+
) -> str:
|
|
266
|
+
parts = [f"A={a_value}"]
|
|
267
|
+
parts.extend(
|
|
268
|
+
f"C[{index[0]}, {index[1]}, {index[2]}]={value}"
|
|
269
|
+
for index, value in sorted(c_values.items())
|
|
270
|
+
)
|
|
271
|
+
return ", ".join(parts)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _sympy_expr_to_c_string(expr: sp.Expr) -> str:
|
|
275
|
+
return sp.sstr(expr)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse_polynomial(expression: str) -> _ParsedPolynomial:
|
|
279
|
+
if not isinstance(expression, str) or not expression.strip():
|
|
280
|
+
raise PolynomialParseError("expression must be a non-empty string.")
|
|
281
|
+
|
|
282
|
+
index_to_token: dict[Index, str] = {}
|
|
283
|
+
token_to_index: dict[str, Index] = {}
|
|
284
|
+
|
|
285
|
+
def replace_c(match: re.Match[str]) -> str:
|
|
286
|
+
index = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
|
287
|
+
token = index_to_token.get(index)
|
|
288
|
+
if token is None:
|
|
289
|
+
token = f"{_TOKEN_PREFIX}{len(index_to_token)}"
|
|
290
|
+
index_to_token[index] = token
|
|
291
|
+
token_to_index[token] = index
|
|
292
|
+
return f" {token} "
|
|
293
|
+
|
|
294
|
+
rewritten = _C_PATTERN.sub(replace_c, expression)
|
|
295
|
+
a_symbol = sp.Symbol("A")
|
|
296
|
+
token_symbols = {token: sp.Symbol(token) for token in token_to_index}
|
|
297
|
+
local_dict = {"A": a_symbol, **token_symbols}
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
parsed = parse_expr(
|
|
301
|
+
rewritten,
|
|
302
|
+
local_dict=local_dict,
|
|
303
|
+
global_dict=dict(_SAFE_GLOBALS),
|
|
304
|
+
transformations=_TRANSFORMATIONS,
|
|
305
|
+
evaluate=True,
|
|
306
|
+
)
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
raise PolynomialParseError(f"could not parse expression: {expression!r}") from exc
|
|
309
|
+
|
|
310
|
+
allowed_symbols = {a_symbol, *token_symbols.values()}
|
|
311
|
+
unknown_symbols = sorted(
|
|
312
|
+
str(symbol) for symbol in parsed.free_symbols if symbol not in allowed_symbols
|
|
313
|
+
)
|
|
314
|
+
if unknown_symbols:
|
|
315
|
+
names = ", ".join(unknown_symbols)
|
|
316
|
+
raise PolynomialParseError(f"unsupported symbol(s): {names}")
|
|
317
|
+
|
|
318
|
+
c_symbols = {
|
|
319
|
+
token_symbols[token]: index
|
|
320
|
+
for token, index in token_to_index.items()
|
|
321
|
+
}
|
|
322
|
+
return _ParsedPolynomial(parsed, a_symbol, c_symbols)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _collect_terms(parsed: _ParsedPolynomial) -> list[_CollectedTerm]:
|
|
326
|
+
expr = sp.expand(parsed.expr)
|
|
327
|
+
c_items = sorted(parsed.c_symbols.items(), key=lambda item: item[1])
|
|
328
|
+
|
|
329
|
+
collected: dict[tuple[tuple[Index, int], ...], sp.Expr] = {}
|
|
330
|
+
for term in sp.Add.make_args(expr):
|
|
331
|
+
c_powers, coefficient = _split_c_factor(term, c_items)
|
|
332
|
+
collected[c_powers] = collected.get(c_powers, sp.Integer(0)) + coefficient
|
|
333
|
+
|
|
334
|
+
terms: list[_CollectedTerm] = []
|
|
335
|
+
for c_powers, coefficient in collected.items():
|
|
336
|
+
normalized = _normalize_a_polynomial(coefficient, parsed.a_symbol)
|
|
337
|
+
if normalized == 0:
|
|
338
|
+
continue
|
|
339
|
+
terms.append(_CollectedTerm(c_powers, normalized))
|
|
340
|
+
|
|
341
|
+
return sorted(terms, key=_term_sort_key)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _split_c_factor(
|
|
345
|
+
term: sp.Expr,
|
|
346
|
+
c_items: list[tuple[sp.Symbol, Index]],
|
|
347
|
+
) -> tuple[tuple[tuple[Index, int], ...], sp.Expr]:
|
|
348
|
+
powers = term.as_powers_dict()
|
|
349
|
+
coefficient = term
|
|
350
|
+
c_powers: list[tuple[Index, int]] = []
|
|
351
|
+
|
|
352
|
+
for symbol, index in c_items:
|
|
353
|
+
power = powers.get(symbol, sp.Integer(0))
|
|
354
|
+
if power == 0:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
int_power = _as_integer_power(
|
|
358
|
+
power,
|
|
359
|
+
"expression must be a Laurent polynomial in the indexed C terms.",
|
|
360
|
+
)
|
|
361
|
+
coefficient /= symbol ** int_power
|
|
362
|
+
c_powers.append((index, int_power))
|
|
363
|
+
|
|
364
|
+
coefficient = sp.cancel(coefficient)
|
|
365
|
+
if any(coefficient.has(symbol) for symbol, _index in c_items):
|
|
366
|
+
raise PolynomialParseError(
|
|
367
|
+
"expression must be a Laurent polynomial in the indexed C terms."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return tuple(c_powers), coefficient
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _normalize_a_polynomial(coefficient: sp.Expr, a_symbol: sp.Symbol) -> sp.Expr:
|
|
374
|
+
expanded = sp.expand(coefficient)
|
|
375
|
+
for term in sp.Add.make_args(expanded):
|
|
376
|
+
power = term.as_powers_dict().get(a_symbol, sp.Integer(0))
|
|
377
|
+
if power == 0:
|
|
378
|
+
a_free_part = term
|
|
379
|
+
else:
|
|
380
|
+
int_power = _as_integer_power(
|
|
381
|
+
power,
|
|
382
|
+
"coefficients must be Laurent polynomials in A.",
|
|
383
|
+
)
|
|
384
|
+
a_free_part = sp.cancel(term / (a_symbol ** int_power))
|
|
385
|
+
|
|
386
|
+
if a_free_part.has(a_symbol):
|
|
387
|
+
raise PolynomialParseError("coefficients must be Laurent polynomials in A.")
|
|
388
|
+
|
|
389
|
+
return sp.collect(expanded, a_symbol)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _as_integer_power(power: sp.Expr, error_message: str) -> int:
|
|
393
|
+
if power.is_integer is not True:
|
|
394
|
+
raise PolynomialParseError(error_message)
|
|
395
|
+
return int(power)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _term_sort_key(term: _CollectedTerm) -> tuple[int, tuple[tuple[Index, int], ...]]:
|
|
399
|
+
return (1, ()) if not term.c_powers else (0, term.c_powers)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _format_terms(terms: list[_CollectedTerm], power_operator: PowerOperator) -> str:
|
|
403
|
+
if not terms:
|
|
404
|
+
return "0"
|
|
405
|
+
|
|
406
|
+
chunks: list[str] = []
|
|
407
|
+
for term in terms:
|
|
408
|
+
coefficient = term.coefficient
|
|
409
|
+
|
|
410
|
+
if term.c_powers:
|
|
411
|
+
body = (
|
|
412
|
+
f"{_format_c_factor(term.c_powers, power_operator)}"
|
|
413
|
+
f"*({_format_expr(coefficient, power_operator)})"
|
|
414
|
+
)
|
|
415
|
+
chunks.append(body if not chunks else f" + {body}")
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
is_negative = coefficient.could_extract_minus_sign()
|
|
419
|
+
displayed_coefficient = -coefficient if is_negative else coefficient
|
|
420
|
+
body = _format_expr(displayed_coefficient, power_operator)
|
|
421
|
+
|
|
422
|
+
if not chunks:
|
|
423
|
+
chunks.append(f"-{body}" if is_negative else body)
|
|
424
|
+
else:
|
|
425
|
+
chunks.append(f" - {body}" if is_negative else f" + {body}")
|
|
426
|
+
|
|
427
|
+
return "".join(chunks)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _format_expr(expr: sp.Expr, power_operator: PowerOperator) -> str:
|
|
431
|
+
a_symbol = sp.Symbol("A")
|
|
432
|
+
terms = [
|
|
433
|
+
_split_a_term(term, a_symbol)
|
|
434
|
+
for term in sp.Add.make_args(sp.expand(expr))
|
|
435
|
+
]
|
|
436
|
+
terms.sort(key=lambda item: item[0], reverse=True)
|
|
437
|
+
|
|
438
|
+
chunks: list[str] = []
|
|
439
|
+
for power, coefficient in terms:
|
|
440
|
+
is_negative = coefficient.could_extract_minus_sign()
|
|
441
|
+
displayed_coefficient = -coefficient if is_negative else coefficient
|
|
442
|
+
body = _format_a_term(displayed_coefficient, power, power_operator)
|
|
443
|
+
|
|
444
|
+
if not chunks:
|
|
445
|
+
chunks.append(f"-{body}" if is_negative else body)
|
|
446
|
+
else:
|
|
447
|
+
chunks.append(f" - {body}" if is_negative else f" + {body}")
|
|
448
|
+
|
|
449
|
+
return "".join(chunks)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _split_a_term(term: sp.Expr, a_symbol: sp.Symbol) -> tuple[int, sp.Expr]:
|
|
453
|
+
power = term.as_powers_dict().get(a_symbol, sp.Integer(0))
|
|
454
|
+
int_power = _as_integer_power(power, "coefficients must be Laurent polynomials in A.")
|
|
455
|
+
coefficient = sp.cancel(term / (a_symbol ** int_power))
|
|
456
|
+
return int_power, coefficient
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _format_a_term(
|
|
460
|
+
coefficient: sp.Expr,
|
|
461
|
+
power: int,
|
|
462
|
+
power_operator: PowerOperator,
|
|
463
|
+
) -> str:
|
|
464
|
+
if power == 0:
|
|
465
|
+
return _format_number(coefficient, power_operator)
|
|
466
|
+
|
|
467
|
+
power_text = _format_a_power(power, power_operator)
|
|
468
|
+
if coefficient == 1:
|
|
469
|
+
return power_text
|
|
470
|
+
|
|
471
|
+
return f"{_format_number(coefficient, power_operator)}*{power_text}"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _format_a_power(power: int, power_operator: PowerOperator) -> str:
|
|
475
|
+
if power == 1:
|
|
476
|
+
return "A"
|
|
477
|
+
if power < 0:
|
|
478
|
+
return f"A{power_operator}({power})"
|
|
479
|
+
return f"A{power_operator}{power}"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _format_number(number: sp.Expr, power_operator: PowerOperator) -> str:
|
|
483
|
+
text = sp.sstr(number)
|
|
484
|
+
if power_operator == "^":
|
|
485
|
+
return text.replace("**", "^")
|
|
486
|
+
return text
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _format_c_factor(
|
|
490
|
+
c_powers: tuple[tuple[Index, int], ...],
|
|
491
|
+
power_operator: PowerOperator,
|
|
492
|
+
) -> str:
|
|
493
|
+
factors: list[str] = []
|
|
494
|
+
for index, power in c_powers:
|
|
495
|
+
factor = _format_indexed_c(index)
|
|
496
|
+
if power != 1:
|
|
497
|
+
factor = f"{factor}{power_operator}{power}"
|
|
498
|
+
factors.append(factor)
|
|
499
|
+
return "*".join(factors)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _format_indexed_c(index: Index) -> str:
|
|
503
|
+
x, y, z = index
|
|
504
|
+
return f"C[{x}, {y}, {z}]"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _c_factor_expr(
|
|
508
|
+
c_powers: tuple[tuple[Index, int], ...],
|
|
509
|
+
c_base: sp.IndexedBase,
|
|
510
|
+
) -> sp.Expr:
|
|
511
|
+
factors: list[sp.Expr] = []
|
|
512
|
+
for (x, y, z), power in c_powers:
|
|
513
|
+
indexed = c_base[x, y, z]
|
|
514
|
+
factors.append(indexed if power == 1 else sp.Pow(indexed, power))
|
|
515
|
+
return sp.Mul(*factors)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: homlab-polynomial
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Collect indexed C terms in polynomials with SymPy.
|
|
5
|
+
Author: GGN_2015
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: sympy>=1.10
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# homlab_polynomial
|
|
14
|
+
|
|
15
|
+
`homlab_polynomial` 提供一个 Python 编程入口,用 SymPy 将包含普通变量 `A` 和三下标变量
|
|
16
|
+
`C[x, y, z]` 的字符串表达式整理成按不同 `C[x, y, z]` 分组的形式。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 使用
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from homlab_polynomial import organize_polynomial
|
|
28
|
+
|
|
29
|
+
expr = "A*C[1, 2, 3] + 2*C[1,2,3] + A^2*C[0,0,0]"
|
|
30
|
+
print(organize_polynomial(expr))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
输出:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
C[0, 0, 0]*(A^2) + C[1, 2, 3]*(A + 2)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
负指数也支持:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
organize_polynomial("A^-10*C[0,0,0] + 2*C[0,0,0]/A")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
输出:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
C[0, 0, 0]*(2*A^(-1) + A^(-10))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
说明:
|
|
52
|
+
|
|
53
|
+
- `C[x, y, z]` 的三个下标必须是整数,可以为负数。
|
|
54
|
+
- 输入支持用 `^` 表示指数运算,也支持 Python/SymPy 风格的 `**`。
|
|
55
|
+
- 默认输出也使用 `^`。如果需要 `**`,可以传入 `output_power_operator="**"`。
|
|
56
|
+
- 系数支持有限 Laurent 多项式,因此 `A^-1`、`1/A`、`A^-10` 都可以使用。
|
|
57
|
+
- `C[x, y, z]` 项不会带外部负号;负号会保留在括号中的系数多项式里。
|
|
58
|
+
- 不含 `C` 的纯 `A` 项会作为最后的余项保留。
|
|
59
|
+
|
|
60
|
+
如果需要继续做 SymPy 计算,可以使用:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from homlab_polynomial import organize_polynomial_expr
|
|
64
|
+
|
|
65
|
+
sympy_expr = organize_polynomial_expr("A*C[1,2,3] + C[1,2,3]")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 验算
|
|
69
|
+
|
|
70
|
+
`organize_polynomial()` 默认会在每次整理后进行随机代入验算。它会给 `A` 和所有出现过的
|
|
71
|
+
`C[x, y, z]` 赋非零有理数,检查整理前后的表达式是否一致。
|
|
72
|
+
|
|
73
|
+
也可以直接调用验算函数:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from homlab_polynomial import verify_simplification
|
|
77
|
+
|
|
78
|
+
verify_simplification(
|
|
79
|
+
"A*C[1,2,3] + C[1,2,3]",
|
|
80
|
+
"C[1, 2, 3]*(A + 1)",
|
|
81
|
+
)
|
|
82
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/homlab_polynomial/__init__.py
|
|
5
|
+
src/homlab_polynomial/core.py
|
|
6
|
+
src/homlab_polynomial.egg-info/PKG-INFO
|
|
7
|
+
src/homlab_polynomial.egg-info/SOURCES.txt
|
|
8
|
+
src/homlab_polynomial.egg-info/dependency_links.txt
|
|
9
|
+
src/homlab_polynomial.egg-info/requires.txt
|
|
10
|
+
src/homlab_polynomial.egg-info/top_level.txt
|
|
11
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sympy>=1.10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
homlab_polynomial
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
|
|
5
|
+
from homlab_polynomial import (
|
|
6
|
+
PolynomialParseError,
|
|
7
|
+
PolynomialVerificationError,
|
|
8
|
+
organize_polynomial,
|
|
9
|
+
organize_polynomial_expr,
|
|
10
|
+
verify_simplification,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrganizePolynomialTests(unittest.TestCase):
|
|
15
|
+
def test_collects_same_indexed_c_terms(self):
|
|
16
|
+
result = organize_polynomial(
|
|
17
|
+
"A*C[1, 2, 3] + 2*C[1,2,3] + A^2*C[0,0,0]"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.assertEqual(
|
|
21
|
+
result,
|
|
22
|
+
"C[0, 0, 0]*(A^2) + C[1, 2, 3]*(A + 2)",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_supports_negative_indices_and_negative_coefficients(self):
|
|
26
|
+
result = organize_polynomial(
|
|
27
|
+
"C[-1,0,+2]*A^2 + 3*A*C[-1,0,2] - C[3,-4,5]"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.assertEqual(
|
|
31
|
+
result,
|
|
32
|
+
"C[-1, 0, 2]*(A^2 + 3*A) + C[3, -4, 5]*(-1)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_keeps_negative_sign_inside_c_coefficient(self):
|
|
36
|
+
result = organize_polynomial("-A*C[-2,0,1] - C[-2,0,1]")
|
|
37
|
+
|
|
38
|
+
self.assertEqual(result, "C[-2, 0, 1]*(-A - 1)")
|
|
39
|
+
|
|
40
|
+
def test_keeps_terms_without_c_as_remainder(self):
|
|
41
|
+
result = organize_polynomial("A^2 + C[0,0,0]*A + 1")
|
|
42
|
+
|
|
43
|
+
self.assertEqual(result, "C[0, 0, 0]*(A) + A^2 + 1")
|
|
44
|
+
|
|
45
|
+
def test_accepts_implicit_multiplication(self):
|
|
46
|
+
result = organize_polynomial("2C[1, 2, 3] A + C[1,2,3]")
|
|
47
|
+
|
|
48
|
+
self.assertEqual(result, "C[1, 2, 3]*(2*A + 1)")
|
|
49
|
+
|
|
50
|
+
def test_can_return_sympy_expression(self):
|
|
51
|
+
result = organize_polynomial_expr("A*C[1,2,3] + C[1,2,3]")
|
|
52
|
+
a = sp.Symbol("A")
|
|
53
|
+
c = sp.IndexedBase("C")
|
|
54
|
+
|
|
55
|
+
self.assertEqual(sp.expand(result - (a + 1) * c[1, 2, 3]), 0)
|
|
56
|
+
|
|
57
|
+
def test_verifies_equivalent_simplification(self):
|
|
58
|
+
self.assertTrue(
|
|
59
|
+
verify_simplification(
|
|
60
|
+
"A*C[1,2,3] + C[1,2,3]",
|
|
61
|
+
"C[1, 2, 3]*(A + 1)",
|
|
62
|
+
random_seed=123,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def test_verification_rejects_mismatch(self):
|
|
67
|
+
with self.assertRaises(PolynomialVerificationError):
|
|
68
|
+
verify_simplification(
|
|
69
|
+
"A*C[1,2,3] + C[1,2,3]",
|
|
70
|
+
"C[1, 2, 3]*(A + 2)",
|
|
71
|
+
random_seed=123,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def test_supports_negative_powers_of_a(self):
|
|
75
|
+
result = organize_polynomial("A^-10*C[0,0,0] + 2*C[0,0,0]/A")
|
|
76
|
+
|
|
77
|
+
self.assertEqual(result, "C[0, 0, 0]*(2*A^(-1) + A^(-10))")
|
|
78
|
+
|
|
79
|
+
def test_formats_one_over_a_as_negative_power(self):
|
|
80
|
+
result = organize_polynomial("C[0,0,0]/A")
|
|
81
|
+
|
|
82
|
+
self.assertEqual(result, "C[0, 0, 0]*(A^(-1))")
|
|
83
|
+
|
|
84
|
+
def test_supports_negative_powers_of_c(self):
|
|
85
|
+
result = organize_polynomial("A*C[0,0,0]^-1 + 2*C[0,0,0]^-1")
|
|
86
|
+
|
|
87
|
+
self.assertEqual(result, "C[0, 0, 0]^-1*(A + 2)")
|
|
88
|
+
|
|
89
|
+
def test_rejects_unknown_symbols(self):
|
|
90
|
+
with self.assertRaises(PolynomialParseError):
|
|
91
|
+
organize_polynomial("B*C[0,0,0]")
|
|
92
|
+
|
|
93
|
+
def test_rejects_non_laurent_coefficients(self):
|
|
94
|
+
with self.assertRaises(PolynomialParseError):
|
|
95
|
+
organize_polynomial("C[0,0,0]/(A + 1)")
|
|
96
|
+
|
|
97
|
+
def test_rejects_non_laurent_c_terms(self):
|
|
98
|
+
with self.assertRaises(PolynomialParseError):
|
|
99
|
+
organize_polynomial("A/(C[0,0,0] + 1)")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|