介绍 poly 模块的领域

本页介绍了SymPy的 sympy.polys 模块中使用的“域”的概念。重点在于介绍如何直接使用这些域,以及理解它们在 Poly 类内部的使用方式。这是一个相对高级的主题,因此,如果您想更初步地了解 Poly 类和 sympy.polys 模块,建议阅读 模块的基本功能。域类的参考文档在 Poly Domains 的参考文档 中。使用域的内部函数在 多项式操作模块的内部机制 中记录。

什么是域?

对于大多数用户来说,域名只在 Poly 的打印输出中真正引人注意:

>>> from sympy import Symbol, Poly
>>> x = Symbol('x')
>>> Poly(x**2 + x)
Poly(x**2 + x, x, domain='ZZ')
>>> Poly(x**2 + x/2)
Poly(x**2 + 1/2*x, x, domain='QQ')

我们看到这里有一个 Poly 的域是 ZZ 表示整数,另一个的域是 QQ 表示有理数。这些表示多项式的系数所取自的“域”。

从高层次来看,域表示形式概念,例如整数集 \(\mathbb{Z}\) 或有理数集 \(\mathbb{Q}\)。这里的“域”指的是数学中的 整环 概念。

在内部,这些域对应于不同的计算实现和多项式所对应表达式的表示。Poly 对象本身有一个内部表示为 list 的系数和一个 domain 属性,表示这些系数的实现:

>>> p = Poly(x**2 + x/2)
>>> p
Poly(x**2 + 1/2*x, x, domain='QQ')
>>> p.domain
QQ
>>> p.rep  
DMP_Python([1, 1/2, 0], QQ)
>>> p.rep.rep  
[1, 1/2, 0]
>>> type(p.rep.rep[0])  
<class 'sympy.external.pythonmpq.PythonMPQ'>

这里的域是 QQ ,它表示在域系统中对有理数的实现。Poly 实例本身有一个 Poly.domain 属性 QQ ,然后是一个 PythonMPQ 系数的列表,其中 PythonMPQ 是实现 QQ 域元素的类。系数列表 [1, 1/2, 0] 给出了多项式表达式 (1)*x**2 + (1/2)*x + (0) 的标准化低级表示。

本页探讨了 SymPy 中定义的不同域,它们的实现方式以及如何使用它们。它介绍了如何直接使用域和域元素,并解释了它们在内部作为 Poly 对象的一部分是如何使用的。这些信息对于 SymPy 的开发者来说比对于 sympy.polys 模块的用户更为相关。

符号表示表达式

数学表达式可以用许多不同的符号方式表示。多项式域的目的是为不同类别的表达式提供合适的实现。本节考虑数学表达式的符号表示的基本方法:“树”、“稠密多项式”和“稀疏多项式”。

树形表示

符号表达式的最一般表示形式是 ,这是大多数普通 SymPy 表达式使用的表示形式,这些表达式是 Expr 的实例(Basic 的子类)。我们可以使用 srepr() 函数查看这种表示形式:

>>> from sympy import Symbol, srepr
>>> x = Symbol('x')
>>> e = 1 + 1/(2 + x**2)
>>> e
1 + 1/(x**2 + 2)
>>> print(srepr(e))
Add(Integer(1), Pow(Add(Pow(Symbol('x'), Integer(2)), Integer(2)), Integer(-1)))

这里,表达式 e 被表示为一个 Add 节点,它有两个子节点 11/(x**2 + 2)。子节点 1 被表示为一个 Integer,另一个子节点被表示为一个 Pow,其基数为 x**2 + 2,指数为 1。然后 x**2 + 2 被表示为一个 Add,其子节点为 x**22,依此类推。通过这种方式,表达式被表示为一棵树,其中内部节点是诸如 AddMulPow 等的操作,而叶节点是诸如 IntegerSymbol 的原子表达式类型。更多关于这种表示的信息,请参阅 高级表达式操作

树形表示是 SymPy 中 Expr 架构的核心。它是一种高度灵活的表示方法,可以表示非常广泛的可能表达式。它还可以以不同的方式表示等价的表达式,例如:

>>> e = x*(x + 1)
>>> e
x*(x + 1)
>>> e.expand()
x**2 + x

这两个表达式虽然等价,但它们的树形表示不同:

>>> print(srepr(e))
Mul(Symbol('x'), Add(Symbol('x'), Integer(1)))
>>> print(srepr(e.expand()))
Add(Pow(Symbol('x'), Integer(2)), Symbol('x'))

能够以不同的方式表示相同的表达式既是优点也是缺点。能够将表达式转换为不同的形式以适应不同的任务是有用的,但非唯一的表示形式使得很难判断两个表达式是否等价,这对许多计算算法来说实际上非常重要。最重要的任务是能够判断一个表达式是否等于零,这在一般情况下是不可判定的(Richardon’s theorem),但在许多重要的特殊情况下是可判定的。

DUP 表示

将允许的表达式集合限制为特殊情况可以实现更高效的符号表示。正如我们已经看到的 Poly 可以将多项式表示为系数列表。这意味着像 x**4 + x + 1 这样的表达式可以简单地表示为 [1, 0, 0, 1, 1]。这种多项式表达式的系数列表表示法被称为“密集单变量多项式”(DUP)表示法。在这种表示法下,乘法、加法和关键的零测试算法可以比相应的树表示法更高效。我们可以通过查看 Poly 实例的 rep.rep 属性来看到这种表示法:

>>> p = Poly(x**4 + x + 1)
>>> p.rep.rep  
[1, 0, 0, 1, 1]

在 DUP 表示法中,不可能以不同的方式表示相同的表达式。x*(x + 1)x**2 + x 之间没有区别,因为两者都只是 [1, 1, 0]。这意味着比较两个表达式很容易:当且仅当它们的系数全部相等时,它们才相等。零测试特别容易:当且仅当所有系数都为零时,多项式为零(当然,我们需要对系数本身进行简单的零测试)。

我们可以使操作 DUP 表示的函数比操作树表示的函数更高效。许多标准 sympy 表达式的操作实际上是通过转换为多项式表示然后执行计算来实现的。一个例子是 factor() 函数:

>>> from sympy import factor
>>> e = 2*x**3 + 10*x**2 + 16*x + 8
>>> e
2*x**3 + 10*x**2 + 16*x + 8
>>> factor(e)
2*(x + 1)*(x + 2)**2

在内部,factor() 会将表达式从树表示形式转换为DUP表示形式,然后使用函数 dup_factor_list:

>>> from sympy import ZZ
>>> from sympy.polys.factortools import dup_factor_list
>>> p = [ZZ(2), ZZ(10), ZZ(16), ZZ(8)]
>>> p
[2, 10, 16, 8]
>>> dup_factor_list(p, ZZ)
(2, [([1, 1], 1), ([1, 2], 2)])

有许多更多以 dup_* 命名的函数用于操作 DUP 表示,这些函数在 多项式操作模块的内部机制 中有文档记录。还有一些以 dmp_* 前缀命名的函数用于操作多元多项式。

DMP 表示

多元多项式(多变量的多项式)可以表示为一个多项式,其系数本身也是多项式。例如 x**2*y + x**2 + x*y + y + 1 可以表示为 x 的多项式,其中系数本身是 y 的多项式,即:(y + 1)*x**2 + (y)*x + (y+1)。由于我们可以用系数列表表示一个多项式,因此多元多项式可以用系数列表的列表来表示:

>>> from sympy import symbols
>>> x, y = symbols('x, y')
>>> p = Poly(x**2*y + x**2 + x*y + y + 1)
>>> p
Poly(x**2*y + x**2 + x*y + y + 1, x, y, domain='ZZ')
>>> p.rep.rep  
[[1, 1], [1, 0], [1, 1]]

这种(列表的…)系数的列表表示法被称为“密集多元多项式”(DMP)表示法。

稀疏多项式表示

我们可以使用一个字典来映射非零单项式项到它们的系数,而不是使用列表。这被称为“稀疏多项式”表示法。我们可以使用 as_dict() 方法来查看这会是什么样子:

>>> Poly(7*x**20 + 8*x + 9).rep.rep  
[7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 9]
>>> Poly(7*x**20 + 8*x + 9).as_dict()
{(0,): 9, (1,): 8, (20,): 7}

这个字典的键是 x 的幂的指数,值是系数,例如 7*x**20 在字典中变为 (20,): 7。键是一个元组,以便在多元情况下,例如 4*x**2*y**3 可以表示为 (2, 3): 4。稀疏表示可以更高效,因为它避免了存储和操作零系数的需要。当生成器(变量)数量较多时,密集表示变得特别低效,此时使用稀疏表示更好:

>>> from sympy import prod
>>> gens = symbols('x:10')
>>> gens
(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9)
>>> p = Poly(prod(gens))
>>> p
Poly(x0*x1*x2*x3*x4*x5*x6*x7*x8*x9, x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, domain='ZZ')
>>> p.rep.rep  
[[[[[[[[[[1, 0], []], [[]]], [[[]]]], [[[[]]]]], [[[[[]]]]]], [[[[[[]]]]]]], [[[[[[[]]]]]]]], [[[[[[[[]]]]]]]]], [[[[[[[[[]]]]]]]]]]
>>> p.as_dict()
{(1, 1, 1, 1, 1, 1, 1, 1, 1, 1): 1}

在最后一个输出中显示的字典表示,从作为幂元组的单项式((1, 1, 1, ...)x0**1 * x1**1, ...)映射到系数 1。与 DMP 表示 相比,我们有一个更加扁平化的数据结构:它是一个只有一个键和值的 dict。对于这个特定的多项式示例,处理稀疏表示的算法可能比密集算法更高效。

SymPy 的多项式模块基于密集和稀疏表示实现了多项式表达式。还有其他不同特殊类表达式的实现,可以用作这些多项式的系数。本页的其余部分讨论了这些表示是什么以及如何使用它们。

域的基本用法

预定义了几个领域,可以直接使用,例如 ZZQQ,它们分别表示整数环 \(\mathbb{Z}\) 和有理数域 \(\mathbb{Q}\)Domain 对象用于构造元素,这些元素随后可以用于普通的算术运算。:

>>> from sympy import ZZ
>>> z1 = ZZ(2)
>>> z1
2
>>> z1 + z1
4
>>> type(z1)  
<class 'int'>
>>> z1 in ZZ
True

基本操作 +, -, 和 * 用于加法、减法和乘法,将对任何域的元素起作用,并生成新的域元素。使用 / (Python 的“真除法”操作符)进行除法并非对所有域都可行,除非已知该域是域,否则不应与域元素一起使用。例如,除以两个 ZZ 元素可能会得到一个 float,这不是 ZZ 的元素:

>>> z1 / z1  
1.0
>>> type(z1 / z1)  
<class 'float'>
>>> ZZ.is_Field
False

对于非字段,/ 的行为也可能因域的基本类型的不同实现而异。例如,使用 SYMPY_GROUND_TYPES=flint 时,除以 ZZ 的两个元素将引发错误,而不是返回浮点数:

>>> z1 / z1  
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for /: 'flint._flint.fmpz' and 'flint._flint.fmpz'

大多数表示非域环的域允许使用Python的整除 // 和取模 % 运算符进行地板除法(余数)。例如在 ZZ 中:

>>> z1 // z1
1
>>> z1 % z1
0

The QQ 域表示有理数领域,并且确实允许除法:

>>> from sympy import QQ
>>> q1 = QQ(1, 2)
>>> q1
1/2
>>> q2 = QQ(2, 3)
>>> q2
2/3
>>> q1 / q2
3/4
>>> type(q1)  
<class 'sympy.external.pythonmpq.PythonMPQ'>

通常,预期与任意域元素一起工作的代码不应使用除法运算符 ///%。只有运算符 +-*``**``(非负整数指数)应假设与任意域元素一起工作。所有其他操作应作为函数从 Domain 对象访问:

>>> ZZ.quo(ZZ(5), ZZ(3))  # 5 // 3
1
>>> ZZ.rem(ZZ(5), ZZ(3))  # 5 % 3
2
>>> ZZ.div(ZZ(5), ZZ(3))  # divmod(5, 3)
(1, 2)
>>> QQ.div(QQ(5), QQ(3))
(5/3, 0)

The exquo() 函数用于计算一个精确的商。这是 a / b 的类似物,但这里的除法预期是精确的(没有余数),否则将引发错误:

>>> QQ.exquo(QQ(5), QQ(3))
5/3
>>> ZZ.exquo(ZZ(4), ZZ(2))
2
>>> ZZ.exquo(ZZ(5), ZZ(3))
Traceback (most recent call last):
...
ExactQuotientFailed: 3 does not divide 5 in ZZ

域元素的确切方法和属性通常不保证超出基本算术操作。不应假设例如 ZZ 将始终为 int 类型。如果安装了 gmpygmpy2,则对于 ZZQQ 将使用 mpzmpq 类型:

>>> from sympy import ZZ, QQ
>>> ZZ(2)  
mpz(2)
>>> QQ(2, 3)  
mpq(2, 3)

mpz 类型在处理大整数运算时比Python的标准 int 类型更快,尽管对于较小的整数,差异并不那么显著。表示有理数的 mpq 类型是用C语言实现的,而不是Python,因此在gmpy未安装时,它比纯Python实现的 QQ 快很多倍。

通常,用于域元素的Python类型可以通过域的 dtype 属性来检查。当安装了gmpy时,ZZ 的dtype是 \(mpz\),这不是一个实际的类型,不能与 \(isinstance\) 一起使用。因此,可以使用 of_type() 方法来检查对象是否是 dtype 的元素。:

>>> z = ZZ(2)
>>> type(z)  
<class 'int'>
>>> ZZ.dtype  
<class 'int'>
>>> ZZ.of_type(z)
True

域元素 vs sympy 表达式

请注意,域元素与普通 sympy 表达式不是同一类型,后者是 Expr 的子类,例如 Integer。普通 sympy 表达式是通过 sympify() 函数创建的。:

>>> from sympy import sympify
>>> z1_sympy = sympify(2)  # Normal sympy object
>>> z1_sympy
2
>>> type(z1_sympy)
<class 'sympy.core.numbers.Integer'>
>>> from sympy import Expr
>>> isinstance(z1_sympy, Expr)
True

在使用域时,重要的是不要将 sympy 表达式与域元素混合,即使有时在简单情况下它会起作用。每个域对象都有方法 to_sympy()from_sympy(),用于在 sympy 表达式和域元素之间来回转换:

>>> z_sympy = sympify(2)
>>> z_zz = ZZ.from_sympy(z_sympy)
>>> z_zz
2
>>> type(z_sympy)
<class 'sympy.core.numbers.Integer'>
>>> type(z_zz)  
<class 'int'>
>>> ZZ.to_sympy(z_zz)
2
>>> type(ZZ.to_sympy(z_zz))
<class 'sympy.core.numbers.Integer'>

任何特定的域只能表示某些 sympy 表达式,因此如果表达式无法在该域中表示,转换将会失败:

>>> from sympy import sqrt
>>> e = sqrt(2)
>>> e
sqrt(2)
>>> ZZ.from_sympy(e)
Traceback (most recent call last):
...
CoercionFailed: expected an integer, got sqrt(2)

我们已经看到,在某些情况下,我们可以使用域对象本身作为构造函数,例如 QQ(2)。只要给定的参数对该域的 dtype 有效,这通常是可行的。虽然在交互式会话和演示中使用这种方法很方便,但通常最好使用 from_sympy() 方法从 sympy 表达式(或可以 sympify 为 sympy 表达式的对象)构造域元素。

重要的是不要将域元素与 intfloat 等其他 Python 类型以及标准的 sympy Expr 表达式混用。在域中工作时,应小心,因为某些 Python 操作会隐式地这样做。例如,sum 函数将使用常规的 int 值零,因此 sum([a, b]) 实际上被评估为 (0 + a) + b,其中 0int 类型。

每个域至少是一个环,如果不是一个域的话,并且因此特别保证有两个元素对应于 \(1\)\(0\)。域对象为这些提供了域元素作为属性 onezero。这些对于像Python的 sum 函数这样的东西很有用,它允许提供一个替代对象作为“零”:

>>> ZZ.one
1
>>> ZZ.zero
0
>>> sum([ZZ(1), ZZ(2)])  # don't do this (even it sometimes works)
3
>>> sum([ZZ(1), ZZ(2)], ZZ.zero) # provide the zero from the domain
3

在某个领域执行计算的标准模式是:

  1. 从表示表达式的 sympy Expr 实例开始。

  2. 选择一个合适的领域,能够代表这些表达。

  3. 使用 from_sympy() 将所有表达式转换为域元素。

  4. 使用域元素执行计算。

  5. 使用 to_sympy() 转换回 Expr

以下是 sum 函数的一个实现,它展示了这些步骤,并对一些整数求和,但使用的是域元素而不是标准的 sympy 表达式:

def sum_domain(expressions_sympy):
    """Sum sympy expressions but performing calculations in domain ZZ"""

    # Convert to domain
    expressions_dom = [ZZ.from_sympy(e) for e in expressions_sympy]

    # Perform calculations in the domain
    result_dom = ZZ.zero
    for e_dom in expressions_dom:
        result_dom += e_dom

    # Convert the result back to Expr
    result_sympy = ZZ.to_sympy(result_dom)
    return result_sympy

高斯整数和高斯有理数

我们目前看到的两个示例域是 ZZQQ,分别表示整数和有理数。还有其他简单的域,如 ZZ_IQQ_I,分别表示 高斯整数高斯有理数。高斯整数是形如 \(a\sqrt{-1} + b\) 的数,其中 \(a\)\(b\) 是整数。高斯有理数的定义类似,只是 \(a\)\(b\) 可以是有理数。我们可以像这样使用高斯域:

>>> from sympy import ZZ_I, QQ_I, I
>>> z = ZZ_I.from_sympy(1 + 2*I)
>>> z
(1 + 2*I)
>>> z**2
(-3 + 4*I)

注意这与树表示中此计算工作方式的对比,在树表示中需要 expand() 来获得简化形式:

>>> from sympy import expand, I
>>> z = 1 + 2*I
>>> z**2
(1 + 2*I)**2
>>> expand(z**2)
-3 + 4*I

The ZZ_IQQ_I 域由类 GaussianIntegerRingGaussianRationalField 实现,它们的元素分别由 GaussianIntegerGaussianRational 表示。ZZ_IQQ_I 的元素的内部表示形式仅仅是 ZZQQ 的元素对 (a, b)。域 ZZ_I 是一个具有与 ZZ 相似性质的环,而 QQ_I 是一个类似于 QQ 的域:

>>> ZZ.is_Field
False
>>> QQ.is_Field
True
>>> ZZ_I.is_Field
False
>>> QQ_I.is_Field
True

由于 QQ_I 是一个域,除以非零元素总是可能的,而在 ZZ_I 中,我们有一个重要的概念,即最大公约数(GCD):

>>> e1 = QQ_I.from_sympy(1+I)
>>> e2 = QQ_I.from_sympy(2-I/2)
>>> e1/e2
(6/17 + 10/17*I)
>>> ZZ_I.gcd(ZZ_I(5), ZZ_I.from_sympy(1+2*I))
(1 + 2*I)

有限域

到目前为止,我们已经看到了域 ZZ, QQ, ZZ_I, 和 QQ_I。还有一些代表 有限域 的域,尽管这些的实现还不完整。一个有限域 GF(p)素数 阶可以通过 FFGF 来构造。一个素数阶 \(p\) 的有限域可以通过 GF(p) 来构造:

>>> from sympy import GF
>>> K = GF(5)
>>> two = K(2)
>>> two 
2 mod 5
>>> two ** 2A 
4 mod 5
>>> two ** 3 
3 mod 5

还有一个 FF 作为 GF 的别名(分别代表“有限域”和“伽罗瓦域”)。它们是等价的,FF(n)GF(n) 都将创建一个域,该域是 FiniteField 的一个实例。关联的域元素将是 PythonFiniteFieldGMPYFiniteField 的实例,具体取决于是否安装了 gmpy

阶为 \(p^n\)\(n e 1\) 的有限域未实现。可以使用例如 GF(6)GF(9),但结果域 不是 一个域。它只是模 69 的整数,因此具有零因子且存在不可逆元素:

>>> K = GF(6)
>>> K(3) * K(2) 
0 mod 6

拥有一个适当实现的素数幂阶有限域会很好,但SymPy中尚未提供此功能(欢迎贡献!)。

实数和复数域

字段 RRCC 在数学上分别对应于 实数复数,即 \(\mathbb{R}\)\(\mathbb{C}\)。这些字段的实现使用了浮点运算。实际上,这意味着这些字段用于表示包含浮点数的表达式。RR 的元素是 mpmathmpf 的实例,并具有一个 _mpf_ 元组,这是 mpmath 中任意浮点实数的表示方式。CC 的元素是 mpc 的实例,并具有一个 _mpc_ 元组,该元组是一对 _mpf_ 元组,分别表示实部和虚部。有关浮点数表示的更多信息,请参阅 mpmath 文档:

>>> from sympy import RR, CC
>>> xr = RR(3)
>>> xr
3.0
>>> xr._mpf_
(0, 3, 0, 2)
>>> zc = CC(3+1j)
>>> zc
(3.0 + 1.0j)
>>> zc._mpc_
((0, 3, 0, 2), (0, 1, 0, 1))

在这些领域中使用近似浮点运算会带来所有常见的陷阱。sympy.polys 模块中的许多算法本质上是为精确运算设计的,因此使用这些领域可能会出现问题:

>>> RR('0.1') + RR('0.2') == RR('0.3')
False

由于这些是使用 mpmath 实现的,而 mpmath 是一个多精度库,因此可以创建具有不同工作精度的不同域。默认域 RRCC 使用 53 位二进制精度,这与标准的 双精度 浮点数非常相似,相当于大约 15 位十进制数字:

>>> from sympy import RealField
>>> RR.precision
53
>>> RR.dps
15
>>> RR(1) / RR(3)
0.333333333333333
>>> RR100 = RealField(100)
>>> RR100.precision   # precision in binary bits
100
>>> RR100.dps         # precision in decimal places
29
>>> RR100(1) / RR100(3)
0.33333333333333333333333333333

代数数域

有理数 \(\mathbb{Q}\)代数扩张 被称为 代数数域 ,这些在 sympy 中通过 QQ<a> 实现。这些的自然语法类似于 QQ(sqrt(2)) 然而 QQ() 已经被重载为 QQ 元素的构造函数。这些域是通过 algebraic_field() 方法创建的,例如 QQ.algebraic_field(sqrt(2))。生成的域将是 AlgebraicField 的一个实例,其元素是 ANP 的实例。

这些的打印支持开发较少,但我们可以使用 to_sympy() 来利用相应的 Expr 打印支持:

>>> K = QQ.algebraic_field(sqrt(2))
>>> K
QQ<sqrt(2)>
>>> b = K.one + K.from_sympy(sqrt(2))
>>> b  
ANP([1, 1], [1, 0, -2], QQ)
>>> K.to_sympy(b)
1 + sqrt(2)
>>> b ** 2  
ANP([2, 3], [1, 0, -2], QQ)
>>> K.to_sympy(b**2)
2*sqrt(2) + 3

原始打印显示立即展示了元素的内部表示,这些元素作为 ANP 实例。域 \(\mathbb{Q}(\sqrt{2})\) 由形如 \(a\sqrt{2}+b\) 的数组成,其中 \(a\)\(b\) 是有理数。因此,该域中的每个数都可以表示为 QQ 中元素的对 (a, b)。域元素将这两个数存储在一个列表中,并且还存储了扩展元素 \(\sqrt{2}\) 的*最小多项式*的列表表示。有一个 sympy 函数 minpoly() 可以计算任何有理数上代数表达式的最小多项式:

>>> from sympy import minpoly, Symbol
>>> x = Symbol('x')
>>> minpoly(sqrt(2), x)
x**2 - 2

在密集多项式表示中,作为系数列表,这个多项式表示为 [1, 0, -2],如上文在 ANP 显示中对 QQ<sqrt(2)> 元素的展示所示。

也可以创建一个具有多个生成元的代数数域,例如 \(\mathbb{Q}(\sqrt{2},\sqrt{3})\):

>>> K = QQ.algebraic_field(sqrt(2), sqrt(3))
>>> K
QQ<sqrt(2) + sqrt(3)>
>>> sqrt2 = K.from_sympy(sqrt(2))
>>> sqrt3 = K.from_sympy(sqrt(3))
>>> p = (K.one + sqrt2) * (K.one + sqrt3)
>>> p  
ANP([1/2, 1, -3/2], [1, 0, -10, 0, 1], QQ)
>>> K.to_sympy(p)
1 + sqrt(2) + sqrt(3) + sqrt(6)
>>> K.to_sympy(p**2)
4*sqrt(6) + 6*sqrt(3) + 8*sqrt(2) + 12

这里,代数扩张 \(\mathbb{Q}(\sqrt{2},\sqrt{3})\) 被转换为(同构的)`mathbb{Q}(sqrt{2}+sqrt{3})`,使用单一生成元 \(\sqrt{2}+\sqrt{3}\)。由于 原始元素定理,总是可以找到这样的单一生成元。有一个 sympy 函数 primitive_element() 可以计算扩张的原始元素的最小多项式:

>>> from sympy import primitive_element, minpoly
>>> e = primitive_element([sqrt(2), sqrt(3)], x)
>>> e[0]
x**4 - 10*x**2 + 1
>>> e[0].subs(x, sqrt(2) + sqrt(3)).expand()
0

最小多项式 x**4 - 10*x**2 + 1 具有密集列表表示 [1, 0, -10, 0, 1],如上文 ANP 输出所示。原始元素定理的含义是,所有代数数域都可以通过某个最小多项式的单一生成元来表示为有理数的扩展。代数数域上的计算只需要利用最小多项式,这使得所有算术运算以及更高级别的操作(如多项式因式分解)成为可能。

多项式环域

还有一些域被实现来表示多项式环,例如 K[x] ,它是在生成元 x 上系数在另一个域 K 上的多项式域:

>>> from sympy import ZZ, symbols
>>> x = symbols('x')
>>> K = ZZ[x]
>>> K
ZZ[x]
>>> x_dom = K(x)
>>> x_dom + K.one
x + 1

前面讨论的所有操作都将适用于多项式环的元素:

>>> p = x_dom + K.one
>>> p
x + 1
>>> p + p
2*x + 2
>>> p - p
0
>>> p * p
x**2 + 2*x + 1
>>> p ** 3
x**3 + 3*x**2 + 3*x + 1
>>> K.exquo(x_dom**2 - K.one, x_dom - K.one)
x + 1

K[x] 元素的内部表示与普通 sympy (Expr) 表达式的表示方式不同。任何表达式的 Expr 表示都是作为一棵树,例如:

>>> from sympy import srepr
>>> K = ZZ[x]
>>> p_expr = x**2 + 2*x + 1
>>> p_expr
x**2 + 2*x + 1
>>> srepr(p_expr)
"Add(Pow(Symbol('x'), Integer(2)), Mul(Integer(2), Symbol('x')), Integer(1))"

这里的表达式是一棵树,其中顶部节点是一个 Add,其子节点是 Pow 等。这种树形表示使得可以用不同的方式表示等价的表达式,例如:

>>> x = symbols('x')
>>> p_expr = x*(x + 1) + x
>>> p_expr
x*(x + 1) + x
>>> p_expr.expand()
x**2 + 2*x

相比之下,域 ZZ[x] 仅表示多项式,并通过简单地存储展开多项式的非零系数(即“稀疏”多项式表示)来实现这一点。特别是 ZZ[x] 的元素表示为 Python dict。它们的类型是 PolyElement,这是 dict 的子类。转换为普通字典可以显示其内部表示:

>>> x = symbols('x')
>>> K = ZZ[x]
>>> x_dom = K(x)
>>> p_dom = K(3)*x_dom**2 + K(2)*x_dom + K(7)
>>> p_dom
3*x**2 + 2*x + 7
>>> dict(p_dom)
{(0,): 7, (1,): 2, (2,): 3}

这种内部形式使得无法表示未展开的乘法,因此 ZZ[x] 中元素的任何乘法总是会被展开:

>>> x = symbols('x')
>>> K = ZZ[x]
>>> x_dom = K(x)
>>> p_expr = x * (x + 1) + x
>>> p_expr
x*(x + 1) + x
>>> p_dom = x_dom * (x_dom + K.one) + x_dom
>>> p_dom
x**2 + 2*x

这些相同的考虑也适用于幂运算:

>>> (x + 1) ** 2
(x + 1)**2
>>> (x_dom + K.one) ** 2
x**2 + 2*x + 1

我们也可以构造多元多项式环:

>>> x, y = symbols('x, y')
>>> K = ZZ[x,y]
>>> xk = K(x)
>>> yk = K(y)
>>> xk**2*yk + xk + yk
x**2*y + x + y

也可以构造嵌套的多项式环(尽管效率较低)。环 K[x][y] 在形式上等价于 K[x,y],尽管它们在 sympy 中的实现方式不同:

>>> K = ZZ[x][y]
>>> p = K(x**2 + x*y + y**2)
>>> p
y**2 + x*y + x**2
>>> dict(p)
{(0,): x**2, (1,): x, (2,): 1}

这里像 x**2 这样的系数是 PolyElement 的实例,因此这是一个 dict,其中值也是字典。完整的表示形式更像是:

>>> {k: dict(v) for k, v in p.items()}
{(0,): {(2,): 1}, (1,): {(1,): 1}, (2,): {(0,): 1}}

多元环域 ZZ[x,y] 有一个更高效的表示方式,即作为一个单一的扁平 dict:

>>> K = ZZ[x,y]
>>> p = K(x**2 + x*y + y**2)
>>> p
x**2 + x*y + y**2
>>> dict(p)
{(0, 2): 1, (1, 1): 1, (2, 0): 1}

随着生成器数量的增加,这些表示之间的效率差异会增大,即 ZZ[x,y,z,t,...]ZZ[x][y][z][t]... 相比。

旧(密集)多项式环

在上一节中,我们看到像 K[x] 这样的多项式环的域表示使用了一个稀疏表示,即将多项式表示为从单项式指数映射到系数的字典。还有一个较旧版本的 K[x] 使用密集的 DMP 表示。我们可以使用 poly_ring()old_poly_ring() 来创建这两个版本的 K[x],其中语法 K[x] 等同于 K.poly_ring(x):

>>> K1 = ZZ.poly_ring(x)
>>> K2 = ZZ.old_poly_ring(x)
>>> K1
ZZ[x]
>>> K2
ZZ[x]
>>> K1 == ZZ[x]
True
>>> K2 == ZZ[x]
False
>>> p1 = K1.from_sympy(x**2 + 1)
>>> p2 = K2.from_sympy(x**2 + 1)
>>> p1
x**2 + 1
>>> p2  
DMP_Python([1, 0, 1], ZZ)
>>> type(K1)
<class 'sympy.polys.domains.polynomialring.PolynomialRing'>
>>> type(p1)
<class 'sympy.polys.rings.PolyElement'>
>>> type(K2)
<class 'sympy.polys.domains.old_polynomialring.GlobalPolynomialRing'>
>>> type(p2)  
<class 'sympy.polys.polyclasses.DMP_Python'>

旧多项式环域的内部表示是 DMP 表示,作为系数列表(列表):

>>> repr(p2)  
'DMP_Python([1, 0, 1], ZZ, ZZ[x])'

多项式的 DMP 表示法最显著的用途是作为 Poly 使用的内部表示(这在文档的这一页后面会讨论)。

PolyRing 对比 PolynomialRing

你可能只想在某个特定的多项式环中进行计算,而不关心实现适用于任意域的东西。在这种情况下,你可以使用 ring() 函数更直接地构造环:

>>> from sympy import ring
>>> K, xr, yr = ring([x, y], ZZ)
>>> K
Polynomial ring in x, y over ZZ with lex order
>>> xr**2 - yr**2
x**2 - y**2
>>> (xr**2 - yr**2) // (xr - yr)
x + y

这里的对象 K 代表环,并且是 PolyRing 的一个实例,但它不是一个 多项式域 (它不是 Domain 的子类的实例,因此不能与 Poly 一起使用)。通过这种方式,域系统中使用的多项式环的实现可以独立于域系统使用。

域系统的目的是提供一个统一的接口,用于处理和转换表达式的不同表示形式。为了使 PolyRing 实现在该上下文中可用,PolynomialRing 类是围绕 PolyRing 类的一个包装器,提供了域系统中预期的接口。这使得多项式环的这种实现可以作为更广泛的代码库的一部分使用,该代码库设计用于处理来自不同域的表达式。多项式环的域是一个与 ring() 返回的环不同的对象,尽管两者具有相同的元素:

>>> K, xr, yr = ring([x, y], ZZ)
>>> K
Polynomial ring in x, y over ZZ with lex order
>>> K2 = ZZ[x,y]
>>> K2
ZZ[x,y]
>>> K2.ring
Polynomial ring in x, y over ZZ with lex order
>>> K2.ring == K
True
>>> K(x+y)
x + y
>>> K2(x+y)
x + y
>>> type(K(x+y))
<class 'sympy.polys.rings.PolyElement'>
>>> type(K2(x+y))
<class 'sympy.polys.rings.PolyElement'>
>>> K(x+y) == K2(x+y)
True

有理函数域

一些域被分类为域,而另一些则不是。域和非域之间的主要区别在于,在域中,任何元素总是可以被任何非零元素除。通常可以通过 get_field() 方法将任何域转换为包含该域的域:

>>> from sympy import ZZ, QQ, symbols
>>> x, y = symbols('x, y')
>>> ZZ.is_Field
False
>>> QQ.is_Field
True
>>> QQ[x]
QQ[x]
>>> QQ[x].is_Field
False
>>> QQ[x].get_field()
QQ(x)
>>> QQ[x].get_field().is_Field
True
>>> QQ.frac_field(x)
QQ(x)

这引入了一种新的域 K(x) ,它表示在生成元 x 上另一个域 K 的有理函数域。使用 () 语法无法构造域 QQ(x) ,因此创建它的最简单方法是使用域方法 frac_field()QQ.frac_field(x))或 get_field()QQ[x].get_field())。frac_field() 方法是更直接的方法。

有理函数域 K(x)RationalField 的一个实例。该域表示形式为 \(p(x) / q(x)\) 的函数,其中 \(p\)\(q\) 是多项式。域元素表示为 K[x] 中的多项式对:

>>> K = QQ.frac_field(x)
>>> xk = K(x)
>>> f = xk / (K.one + xk**2)
>>> f
x/(x**2 + 1)
>>> f.numer
x
>>> f.denom
x**2 + 1
>>> QQ[x].of_type(f.numer)
True
>>> QQ[x].of_type(f.denom)
True

分子和分母之间的抵消在这个领域是自动的:

>>> p1 = xk**2 - 1
>>> p2 = xk - 1
>>> p1
x**2 - 1
>>> p2
x - 1
>>> p1 / p2
x + 1

计算这种消去可能会很慢,这使得有理函数域可能比多项式环或代数域更慢。

就像在多项式环的情况下一样,分数域既有新的(稀疏)版本,也有旧的(密集)版本:

>>> K1 = QQ.frac_field(x)
>>> K2 = QQ.old_frac_field(x)
>>> K1
QQ(x)
>>> K2
QQ(x)
>>> type(K1)
<class 'sympy.polys.domains.fractionfield.FractionField'>
>>> type(K2)
<class 'sympy.polys.domains.old_fractionfield.FractionField'>

同样地,就像多项式环的情况一样,有理函数域的实现可以独立于域系统使用:

>>> from sympy import field
>>> K, xf, yf = field([x, y], ZZ)
>>> xf / (1 - yf)
-x/(y - 1)

这里 KFracField 的一个实例,而不是 RationalField ,正如它在 ZZ(x,y) 域中那样。

表达式域

最后需要考虑的领域是“表达式领域”,即 EX。无法使用其他领域表示的表达式始终可以使用表达式领域来表示。EX 的元素实际上只是围绕 Expr 实例的一个包装器:

>>> from sympy import EX
>>> p = EX.from_sympy(1 + x)
>>> p
EX(x + 1)
>>> type(p)
<class 'sympy.polys.domains.expressiondomain.ExpressionDomain.Expression'>
>>> p.ex
x + 1
>>> type(p.ex)
<class 'sympy.core.add.Add'>

对于其他领域,表达式的领域表示通常比 Expr 使用的树表示更高效。在 EX 中,内部表示是 Expr,因此显然不是更高效的。EX 领域的主要目的是能够在一个与其它领域一致的接口中封装任意表达式。当找不到合适的领域时,EX 领域被用作后备。尽管这并不提供任何特别的效率,但它确实允许在处理没有适当领域表示的表达式时,使用实现于任意领域上的算法。

选择一个领域

在上文描述的工作流程中,思路是从一些 sympy 表达式开始,选择一个域并将所有表达式转换到该域中以执行某些计算。显而易见的问题是如何选择一个合适的域来表示某些 sympy 表达式。为此,有一个函数 construct_domain(),它接受一个表达式列表,并会选择一个域并将所有表达式转换到该域中:

>>> from sympy import construct_domain, Integer
>>> elements_sympy = [Integer(3), Integer(2)]  # elements as Expr instances
>>> elements_sympy
[3, 2]
>>> K, elements_K = construct_domain(elements_sympy)
>>> K
ZZ
>>> elements_K
[3, 2]
>>> type(elements_sympy[0])
<class 'sympy.core.numbers.Integer'>
>>> type(elements_K[0])  
<class 'int'>

在这个例子中,我们看到两个整数 32 可以在域 ZZ 中表示。这些表达式已经被转换为该域的元素,在这种情况下意味着 int 类型而不是 Expr 的实例。当输入可以被简化时,不需要显式创建 Expr 实例,例如 construct_domain([3, 2]) 将给出与上述相同的输出。

给定更复杂的输入 construct_domain() 将选择更复杂的域:

>>> from sympy import Rational, symbols
>>> x, y = symbols('x, y')
>>> construct_domain([Rational(1, 2), Integer(3)])[0]
QQ
>>> construct_domain([2*x, 3])[0]
ZZ[x]
>>> construct_domain([x/2, 3])[0]
QQ[x]
>>> construct_domain([2/x, 3])[0]
ZZ(x)
>>> construct_domain([x, y])[0]
ZZ[x,y]

如果在输入中发现任何非整数的有理数,那么基础域将是 QQ 而不是 ZZ。如果在输入中发现任何符号,那么将创建一个 PolynomialRing。如果输入中有多个符号,也可以创建一个多元多项式环,例如 QQ[x,y]。如果任何符号出现在分母中,那么将创建一个类似于 QQ(x)RationalField

上述领域中有些是域,而其他是(非域)环。在某些情况下,需要一个域领域以便进行除法,为此 construct_domain() 有一个选项 field=True,即使表达式都可以在非域环中表示,该选项也会强制构建一个域领域:

>>> construct_domain([1, 2], field=True)[0]
QQ
>>> construct_domain([2*x, 3], field=True)[0]
ZZ(x)
>>> construct_domain([x/2, 3], field=True)[0]
ZZ(x)
>>> construct_domain([2/x, 3], field=True)[0]
ZZ(x)
>>> construct_domain([x, y], field=True)[0]
ZZ(x,y)

默认情况下,construct_domain() 不会构建一个代数扩展域,而是使用 EX 域 (ExpressionDomain)。关键字参数 extension=True 可以用于构建一个 AlgebraicField,如果输入是无理数但代数的话:

>>> from sympy import sqrt
>>> construct_domain([sqrt(2)])[0]
EX
>>> construct_domain([sqrt(2)], extension=True)[0]
QQ<sqrt(2)>
>>> construct_domain([sqrt(2), sqrt(3)], extension=True)[0]
QQ<sqrt(2) + sqrt(3)>

当输入中存在代数无关的超越数时,将构造 PolynomialRingRationalField,将这些超越数视为生成元:

>>> from sympy import sin, cos
>>> construct_domain([sin(x), y])[0]
ZZ[y,sin(x)]

然而,如果输入可能不是代数独立的,那么域将是 EX:

>>> construct_domain([sin(x), cos(x)])[0]
EX

这里 sin(x)cos(x) 不是代数独立的,因为 sin(x)**2 + cos(x)**2 = 1

在不同领域之间转换元素

在不同域上执行的计算通常很有用。然而,正如避免将域元素与普通 sympy 表达式和其他 Python 类型混合使用一样重要,避免将不同域的元素混合使用也同样重要。convert_from() 方法用于将一个域的元素转换为另一个域的元素:

>>> num_zz = ZZ(3)
>>> ZZ.of_type(num_zz)
True
>>> num_qq = QQ.convert_from(num_zz, ZZ)
>>> ZZ.of_type(num_qq)
False
>>> QQ.of_type(num_qq)
True

可以调用 convert() 方法,而不需要指定第二个参数作为源域,例如:

>>> QQ.convert(ZZ(2))
2

这是因为 convert() 可以检查 ZZ(2) 的类型,并尝试确定它属于哪个域(ZZ)。某些域如 ZZQQ 被视为特殊情况以使此功能生效。更复杂域的元素是 DomainElement 的子类的实例,该类具有 parent() 方法,可以识别元素所属的域。例如,在多项式环 ZZ[x] 中,我们有:

>>> from sympy import ZZ, Symbol
>>> x = Symbol('x')
>>> K = ZZ[x]
>>> K
ZZ[x]
>>> p = K(x) + K.one
>>> p
x + 1
>>> type(p)
<class 'sympy.polys.rings.PolyElement'>
>>> p.parent()
ZZ[x]
>>> p.parent() == K
True

然而,调用 convert_from() 并指定源域作为第二个参数会更高效:

>>> QQ.convert_from(ZZ(2), ZZ)
2

统一领域

当我们想要结合来自两个不同领域的元素并对它们进行混合计算时,我们需要

  1. 选择一个能够代表两者所有元素的新领域。

  2. 将所有元素转换到新域。

  3. 在新域中执行计算。

从第1点引发的关键问题是,如何选择一个能够代表两个领域元素的领域。为此,有 unify() 方法:

>>> x1, K1 = ZZ(2), ZZ
>>> y2, K2 = QQ(3, 2), QQ
>>> K1
ZZ
>>> K2
QQ
>>> K3 = K1.unify(K2)
>>> K3
QQ
>>> x3 = K3.convert_from(x1, K1)
>>> y3 = K3.convert_from(y2, K2)
>>> x3 + y3
7/2

The unify() 方法将找到一个包含两个域的域,因此在示例中 ZZ.unify(QQ) 给出 QQ,因为 ZZ 的每个元素都可以表示为 QQ 的元素。这意味着所有输入(x1y2)都可以转换为公共域 K3 的元素(如 x3y3)。一旦进入公共域,我们可以安全地使用算术运算,如 +。在这个示例中,一个域是另一个域的超集,我们看到 K1.unify(K2) == K2,因此实际上不需要转换 y2。但一般来说,K1.unify(K2) 可以给出一个既不等于 K1 也不等于 K2 的新域。

unify() 方法知道如何合并不同的多项式环域以及如何统一基域:

>>> ZZ[x].unify(ZZ[y])
ZZ[x,y]
>>> ZZ[x,y].unify(ZZ[y])
ZZ[x,y]
>>> ZZ[x].unify(QQ)
QQ[x]

也可以将代数域和有理函数域统一起来,如下所示:

>>> K1 = QQ.algebraic_field(sqrt(2))[x]
>>> K2 = QQ.algebraic_field(sqrt(3))[y]
>>> K1
QQ<sqrt(2)>[x]
>>> K2
QQ<sqrt(3)>[y]
>>> K1.unify(K2)
QQ<sqrt(2) + sqrt(3)>[x,y]
>>> QQ.frac_field(x).unify(ZZ[y])
ZZ(x,y)

多边形的内部

我们现在可以理解 Poly 类是如何在内部工作的。这是 Poly 的公共接口:

>>> from sympy import Poly, symbols, ZZ
>>> x, y, z, t = symbols('x, y, z, t')
>>> p = Poly(x**2 + 1, x, domain=ZZ)
>>> p
Poly(x**2 + 1, x, domain='ZZ')
>>> p.gens
(x,)
>>> p.domain
ZZ
>>> p.all_coeffs()
[1, 0, 1]
>>> p.as_expr()
x**2 + 1

这是 Poly 的内部实现:

>>> d = p.rep  # internal representation of Poly
>>> d  
DMP_Python([1, 0, 1], ZZ)
>>> d.rep      # internal representation of DMP  
[1, 0, 1]
>>> type(d.rep)  
<class 'list'>
>>> type(d.rep[0])  
<class 'int'>
>>> d.dom
ZZ

一个 Poly 实例的内部表示是 DMP 的一个实例,这是在旧的多项式环域 old_poly_ring() 中用于域元素的类。这表示多项式为一个系数列表,这些系数本身是某个域的元素,并保留对它们域的引用(在本例中为 ZZ)。

为 Poly 选择一个域名

如果未为 Poly 构造函数指定域,则使用 construct_domain() 推断该域。像 field=True 这样的参数会传递给 construct_domain():

>>> from sympy import sqrt
>>> Poly(x**2 + 1, x)
Poly(x**2 + 1, x, domain='ZZ')
>>> Poly(x**2 + 1, x, field=True)
Poly(x**2 + 1, x, domain='QQ')
>>> Poly(x**2/2 + 1, x)
Poly(1/2*x**2 + 1, x, domain='QQ')
>>> Poly(x**2 + sqrt(2), x)
Poly(x**2 + sqrt(2), x, domain='EX')
>>> Poly(x**2 + sqrt(2), x, extension=True)
Poly(x**2 + sqrt(2), x, domain='QQ<sqrt(2)>')

即使不需要扩展来表示系数,也可以使用扩展参数来指定扩展的生成器,尽管这在直接使用 construct_domain() 时不起作用。扩展元素列表将被传递给 primitive_element() 以创建适当的 AlgebraicField 域:

>>> from sympy import construct_domain
>>> Poly(x**2 + 1, x)
Poly(x**2 + 1, x, domain='ZZ')
>>> Poly(x**2 + 1, x, extension=sqrt(2))
Poly(x**2 + 1, x, domain='QQ<sqrt(2)>')
>>> Poly(x**2 + 1, x, extension=[sqrt(2), sqrt(3)])
Poly(x**2 + 1, x, domain='QQ<sqrt(2) + sqrt(3)>')
>>> construct_domain([1, 0, 1], extension=sqrt(2))[0]
ZZ

(也许 construct_domain() 在这里应该和 Poly 做同样的事情…)

选择生成器

如果存在除生成器以外的符号,则会创建一个多项式环或有理函数域域。在这种情况下,用于系数的域是稀疏(“新”)多项式环:

>>> p = Poly(x**2*y + z, x)
>>> p
Poly(y*x**2 + z, x, domain='ZZ[y,z]')
>>> p.gens
(x,)
>>> p.domain
ZZ[y,z]
>>> p.domain == ZZ[y,z]
True
>>> p.domain == ZZ.poly_ring(y, z)
True
>>> p.domain == ZZ.old_poly_ring(y, z)
False
>>> p.rep.rep  
[y, 0, z]
>>> p.rep.rep[0]  
y
>>> type(p.rep.rep[0])  
<class 'sympy.polys.rings.PolyElement'>
>>> dict(p.rep.rep[0])  
{(1, 0): 1}

这里我们有一个密集和稀疏实现的奇怪混合体。Poly 实例认为自己是生成器 x 中的单变量多项式,但其系数来自域 ZZ[y,z]Poly 的内部表示是一个“密集单变量多项式”(DUP)格式的系数列表。然而,每个系数都作为 yz 中的稀疏多项式实现。

如果我们让 xyz 都成为 Poly 的生成器,那么我们将得到一个完全密集的 DMP 列表的列表的列表表示:

>>> p = Poly(x**2*y + z, x, y, z)
>>> p
Poly(x**2*y + z, x, y, z, domain='ZZ')
>>> p.rep
DMP_Python([[[1], []], [[]], [[1, 0]]], ZZ)
>>> p.rep.rep  
[[[1], []], [[]], [[1, 0]]]
>>> p.rep.rep[0][0][0]  
1
>>> type(p.rep.rep[0][0][0])  
<class 'int'>

另一方面,我们可以通过选择一个完全不在表达式中的生成器来创建一个具有完全稀疏表示的 Poly:

>>> p = Poly(x**2*y + z, t)
>>> p
Poly(x**2*y + z, t, domain='ZZ[x,y,z]')
>>> p.rep
DMP_Python([x**2*y + z], ZZ[x,y,z])
>>> p.rep.rep[0]  
x**2*y + z
>>> type(p.rep.rep[0])  
<class 'sympy.polys.rings.PolyElement'>
>>> dict(p.rep.rep[0])  
{(0, 0, 1): 1, (2, 1, 0): 1}

如果没有为 Poly 构造函数提供生成器,那么它将尝试选择生成器,以便表达式是这些生成器的多项式。在表达式是某些符号的多项式表达式的常见情况下,这些符号将被视为生成器。然而,其他非符号表达式也可以被视为生成器:

>>> Poly(x**2*y + z)
Poly(x**2*y + z, x, y, z, domain='ZZ')
>>> from sympy import pi, exp
>>> Poly(exp(x) + exp(2*x) + 1)
Poly((exp(x))**2 + (exp(x)) + 1, exp(x), domain='ZZ')
>>> Poly(pi*x)
Poly(x*pi, x, pi, domain='ZZ')
>>> Poly(pi*x, x)
Poly(pi*x, x, domain='ZZ[pi]')

代数相关生成元

exp(x)pi 作为 Poly 的生成元或其多项式环域的生成元在数学上是有效的,因为这些对象是超越的,因此包含它们的环扩张同构于一个多项式环。由于 xexp(x) 是代数无关的,因此将两者同时作为同一个 Poly 的生成元也是有效的。然而,其他一些生成元组合是无效的,例如 xsqrt(x)sin(x)cos(x)。这些例子是无效的,因为生成元不是代数无关的(例如 sqrt(x)**2 = xsin(x)**2 + cos(x)**2 = 1)。尽管如此,实现无法检测这些代数关系:

>>> from sympy import sin, cos, sqrt
>>> Poly(x*exp(x))      # fine
Poly(x*(exp(x)), x, exp(x), domain='ZZ')
>>> Poly(sin(x)+cos(x)) # not fine
Poly((cos(x)) + (sin(x)), cos(x), sin(x), domain='ZZ')
>>> Poly(x + sqrt(x))   # not fine
Poly(x + (sqrt(x)), x, sqrt(x), domain='ZZ')

使用 Poly 进行的计算,例如这个,是不可靠的,因为在此实现中零测试将无法正常工作:

>>> p1 = Poly(x, x, sqrt(x))
>>> p2 = Poly(sqrt(x), x, sqrt(x))
>>> p1
Poly(x, x, sqrt(x), domain='ZZ')
>>> p2
Poly((sqrt(x)), x, sqrt(x), domain='ZZ')
>>> p3 = p1 - p2**2
>>> p3                  # should be zero...
Poly(x - (sqrt(x))**2, x, sqrt(x), domain='ZZ')
>>> p3.as_expr()
0

可以通过以下方式改进 Poly 的这一方面:

  1. 通过引入新的域来扩展域系统,这些新域可以表示更多类别的代数扩展。

  2. 改进在 construct_domain() 中代数依赖关系的检测。

  3. 改进生成器的自动选择。

上述的例子是,拥有一个可以表示更一般代数扩张的域会很有用(AlgebraicField 仅用于 QQ 的扩张)。改进代数依赖的检测更困难,但至少像 sin(x)cos(x) 这样的常见情况可以处理。在选择生成元时,应该能够识别到 sqrt(x) 可以是 x + sqrt(x) 的唯一生成元:

>>> Poly(x + sqrt(x))            # this could be improved!
Poly(x + (sqrt(x)), x, sqrt(x), domain='ZZ')
>>> Poly(x + sqrt(x), sqrt(x))   # this could be improved!
Poly((sqrt(x)) + x, sqrt(x), domain='ZZ[x]')