42 个稳定版本
2.2.0 | 2024 年 8 月 14 日 |
---|---|
2.1.10 | 2024 年 7 月 28 日 |
1.7.5 | 2024 年 6 月 11 日 |
1.5.3 | 2024 年 3 月 11 日 |
1.3.0 | 2023 年 10 月 29 日 |
在 数学 中排名第 29
每月下载量 1,837
用于 he-ring
57MB
32K SLoC
feanor-math
这是一个完全用 Rust 编写的数论库。目的是提供一个比 NTL 或 FLINT 等项目更现代的替代品,然而由于这些项目范围广泛,当前的实现还远远达不到这个目标。更具体地说,我们使用现代语言特性——特别是特质系统——来提供环的通用框架,这使得嵌套环或创建自定义环变得容易,同时仍然保持高性能。在 NTL 中这是不可能的,虽然 FLINT 提供了这个框架,但它相当复杂,并且由于 FLINT 是用 C 编写的,依赖于运行时多态性——这阻止了彻底的类型检查。从用户的角度来看,我们希望这个库在某种程度上更接近像 sagemath 这样的高级计算机代数系统,但同时也具有强大的静态类型系统和非常高的性能。
当前状态
当前状态与这一愿景还有很大差距,因为只有一些最重要的算法已经实现。此外,可能存在错误,并且许多实现尚未特别优化。尽管如此,我认为这个库已经很有用,我经常使用它来处理各种应用,包括密码学。请注意,除非它们带有 #[stability::unstable]
标记,否则我会努力保持接口稳定。
这个库使用 nightly Rust,甚至包括像 const-generics 和 specialization 这样的不稳定功能——这造成了太多的麻烦,所以我再次移除了这些使用。
简要介绍
该库的设计考虑了环及其算术作为最基本的概念。这导致了两个特质的产生:RingBase
和RingStore
。这两个特质反映了在此crate中的一般设计策略,即提供面向实现者(例如RingBase
)和面向用户(例如RingStore
)的特质。前者试图使实现具有自定义算术的新环变得容易,后者使编写在环内执行算术的代码变得容易。更多关于这种分离的原因将在本页的下方进行解释。
功能
以下提供了以下环
- 整数环
Z
,作为一个特质crate::integer::IntegerRing
,对于所有原始整数(从i8到
i128
)都有实现,一个任意精度的实现crate::rings::rust_bigint::RustBigintRing
,以及一个使用绑定到高度优化的库mpir
的可选实现(通过features=mpir
启用)。 - 商环
Z/nZ
,作为一个特质crate::rings::zn::ZnRing
,有四种实现。其中模数较小且在编译时已知crate::rings::zn::zn_static::Zn
,小于64位的模数优化的Barett约简实现crate::rings::zn::zn_64::Zn
,任何模数和任何整数环(包括任意精度环)的Barett约简的泛型实现crate::rings::zn::zn_big::Zn
,以及高度复合模数的剩余数系统实现crate::rings::zn::zn_rns::Zn
。 - 任何基环上的多项式环
R[X]
,作为一个具有两个实现(稠密多项式crate::rings::poly::dense_poly::DensePolyRing
和稀疏多项式crate::rings::poly::sparse_poly::SparsePolyRing
)的特质crate::rings::poly::PolyRing
。 - 有限秩的简单自由环扩张,作为一个基于多项式除法的特质
crate::rings::extension::FreeAlgebra
,具体包括有限/伽罗瓦域和数域。 - 任何基环上的多元多项式环
R[X1, ..., XN]
,作为特质crate::rings::multivariate::MultivariatePolyRing
和一个基于有序向量的稀疏表示的实现crate::rings::multivariate::ordered::MultivariatePolyRingImpl
。 - 结合以上内容,您可以获得伽罗瓦域(使用 [
crate::rings::extension::galois_field::GF()
] 容易获得)或任意数域。
以下算法已实现
- 快速傅里叶变换,包括对二的幂次情况的Cooley-Tuckey算法的优化实现,对任意长度的Bluestein算法的实现,以及基于Cooley-Tuckey算法的因子FFT实现。傅里叶变换适用于所有具有合适单位根的环,特别是复数
C
和合适的有限环Fq
。 - 用于快速卷积的Karatsuba算法的优化变体。
- 用于有限域上分解多项式的Cantor-Zassenhaus算法的实现。
- 有理数/整数上的多项式分解(使用Hensel提升)和数域上的多项式分解。
- Lenstra的椭圆曲线算法分解整数(目前非常慢)。
- 用于格减小的LLL算法。
- 主理想环上的基本线性代数。
- 用于检查整数素性的Miller-Rabin测试。
- 基于婴儿步-巨人步和分解算法计算任意离散对数。
- Faugere的F4算法计算Gröbner基。
遗憾的是,目前对无限环(整数、有理数、数域)上的多项式操作非常慢,因为有效的实现需要大量小心以防止系数膨胀,而我没有时间和需求去投资。
最重要的缺失功能
- 矩阵和线性代数的全面处理。目前只有矩阵和线性代数的一个非常简约的抽象
crate::matrix
,主要供内部使用。 - 对无限环上的多项式进行仔细处理,主要使用专门实现以防止系数膨胀。
格减小和LLL算法。这也可能是上述点所需的内容。现在已实现!更仔细设计的内存分配抽象(最好是使用新的crate现在使用Rust的memory-provider
或类似)。allocator-api
以及feanor-mempool
!- 更多的数论算法,例如计算Galois群。我还没有确定在哪里划线,因为我认为高级数论算法(椭圆曲线、类群、...)不在这个项目的范围之内。技术上我会把整数分解包括在内,但它对其他算法来说太重要了。
语义版本
在版本 1..x.x
中,库使用了不一致的版本方案。现在已修复,并且从版本 2..x.x
开始使用语义版本,如Cargo手册中所述。请注意,所有标记为#[stability::unstable]
的rust库中的项目都免于语义版本。换句话说,这些结构/特质/函数接口的破坏性更改只会增加次要版本号。请注意,除非激活了功能 unstable-enable
,否则这些更改对其他crate不可见。
类似项目
最近我了解到一个名为 Symbolica 的项目,它与 Rust 中的符号计算方法类似。与主要用于数论计算的 feanor-math 不同,Symbolica 主要关注多变量多项式(包括浮点数)的计算,在这个领域比 feanor-math 更优化。如果它更适合您的用例,不妨去看看!我个人认为这是一个非常棒的项目。
示例
使用环
以下是一个如何使用库的简单示例,我们在这里实现了费马素性检验
use feanor_math::homomorphism::*;
use feanor_math::assert_el_eq;
use feanor_math::ring::*;
use feanor_math::primitive_int::*;
use feanor_math::rings::zn::zn_big::*;
use feanor_math::rings::zn::*;
use feanor_math::rings::finite::*;
use feanor_math::algorithms;
use oorandom;
fn fermat_is_prime(n: i64) -> bool {
// the Fermat primality test is based on the observation that a^n == a mod n if n
// is a prime; On the other hand, if n is not prime, we hope that there are many
// a such that this is not the case.
// Note that this is not always the case, and so more advanced primality tests should
// be used in practice. This is just a proof of concept.
let ZZ = StaticRing::<i64>::RING;
let Zn = Zn::new(ZZ, n); // the ring Z/nZ
// check for 6 random a whether a^n == a mod n
let mut rng = oorandom::Rand64::new(0);
for _ in 0..6 {
let a = Zn.random_element(|| rng.rand_u64());
let a_n = Zn.pow(Zn.clone_el(&a), n as usize);
if !Zn.eq_el(&a, &a_n) {
return false;
}
}
return true;
}
assert!(algorithms::miller_rabin::is_prime(StaticRing::<i64>::RING, &91, 6) == fermat_is_prime(91));
如果我们想支持任意整数环 - 例如 BigIntRing::RING
,它是对任意精度整数的简单实现 - 我们可以将函数泛型化
use feanor_math::homomorphism::*;
use feanor_math::ring::*;
use feanor_math::integer::*;
use feanor_math::integer::*;
use feanor_math::rings::zn::zn_big::*;
use feanor_math::rings::zn::*;
use feanor_math::rings::finite::*;
use feanor_math::algorithms;
use oorandom;
fn fermat_is_prime<R>(ZZ: R, n: El<R>) -> bool
where R: RingStore, R::Type: IntegerRing
{
// the Fermat primality test is based on the observation that a^n == a mod n if n
// is a prime; On the other hand, if n is not prime, we hope that there are many
// a such that this is not the case.
// Note that this is not always the case, and so more advanced primality tests should
// be used in practice. This is just a proof of concept.
// ZZ is not guaranteed to be Copy anymore, so use reference instead
let Zn = Zn::new(&ZZ, ZZ.clone_el(&n)); // the ring Z/nZ
// check for 6 random a whether a^n == a mod n
let mut rng = oorandom::Rand64::new(0);
for _ in 0..6 {
let a = Zn.random_element(|| rng.rand_u64());
// use a generic square-and-multiply powering function that works with any implementation
// of integers
let a_n = Zn.pow_gen(Zn.clone_el(&a), &n, &ZZ);
if !Zn.eq_el(&a, &a_n) {
return false;
}
}
return true;
}
// the miller-rabin primality test is implemented in feanor_math::algorithms, so we can
// check our implementation
let n = BigIntRing::RING.int_hom().map(91);
assert!(algorithms::miller_rabin::is_prime(BigIntRing::RING, &n, 6) == fermat_is_prime(BigIntRing::RING, n));
这个函数现在可以与任何实现了 IntegerRing
的环一起工作,它是 RingBase
的子特质。
实现环
要实现自定义环,只需创建一个结构体并添加一个 impl RingBase
和一个 impl CanIsoFromTo<Self>
- 就这么简单!假设我们想提供自己实现的有限二元域 F2,可以这样做。
use feanor_math::homomorphism::*;
use feanor_math::integer::*;
use feanor_math::assert_el_eq;
use feanor_math::ring::*;
#[derive(PartialEq)]
struct F2Base;
impl RingBase for F2Base {
type Element = u8;
fn clone_el(&self, val: &Self::Element) -> Self::Element {
*val
}
fn add_assign(&self, lhs: &mut Self::Element, rhs: Self::Element) {
*lhs = (*lhs + rhs) % 2;
}
fn negate_inplace(&self, lhs: &mut Self::Element) {
*lhs = (2 - *lhs) % 2;
}
fn mul_assign(&self, lhs: &mut Self::Element, rhs: Self::Element) {
*lhs = (*lhs * rhs) % 2;
}
fn from_int(&self, value: i32) -> Self::Element {
// make sure that we handle negative numbers correctly
(((value % 2) + 2) % 2) as u8
}
fn eq_el(&self, lhs: &Self::Element, rhs: &Self::Element) -> bool {
// elements are always represented by 0 or 1
*lhs == *rhs
}
fn characteristic<I>(&self, ZZ: &I) -> Option<El<I>>
where I: IntegerRingStore, I::Type: IntegerRing
{
Some(ZZ.int_hom().map(2))
}
fn is_commutative(&self) -> bool { true }
fn is_noetherian(&self) -> bool { true }
fn dbg<'a>(&self, value: &Self::Element, out: &mut std::fmt::Formatter<'a>) -> std::fmt::Result {
write!(out, "{}", *value)
}
}
pub const F2: RingValue<F2Base> = RingValue::from(F2Base);
assert_el_eq!(F2, F2.int_hom().map(1), F2.add(F2.one(), F2.zero()));
一起使用
这个特质的其中一个主要目标是为了使嵌套环变得容易,因此在环的范畴上实现了一个函子。经典的例子是对于任何环 R
都存在的多项式环 R[X]
。由于在这种情况下我们既在使用也在实现环,我们应该使用框架的两面。例如,一个简单的多项式环实现可能看起来像这样。
use feanor_math::assert_el_eq;
use feanor_math::ring::*;
use feanor_math::integer::*;
use feanor_math::homomorphism::*;
use feanor_math::rings::zn::*;
use std::cmp::{min, max};
pub struct MyPolyRing<R: RingStore> {
base_ring: R
}
impl<R: RingStore> PartialEq for MyPolyRing<R> {
fn eq(&self, other: &Self) -> bool {
self.base_ring.get_ring() == other.base_ring.get_ring()
}
}
impl<R: RingStore> RingBase for MyPolyRing<R> {
// in a real implementation, we might want to wrap this in a newtype, to avoid
// exposing the vector interface (although exposing that interface might be intended -
// the crate does not judge whether this is a good idea)
type Element = Vec<El<R>>;
fn clone_el(&self, el: &Self::Element) -> Self::Element {
el.iter().map(|x| self.base_ring.clone_el(x)).collect()
}
fn add_assign(&self, lhs: &mut Self::Element, rhs: Self::Element) {
for i in 0..min(lhs.len(), rhs.len()) {
self.base_ring.add_assign_ref(&mut lhs[i], &rhs[i]);
}
for i in lhs.len()..rhs.len() {
lhs.push(self.base_ring.clone_el(&rhs[i]))
}
}
fn negate_inplace(&self, val: &mut Self::Element) {
for x in val {
self.base_ring.negate_inplace(x);
}
}
fn mul_ref(&self, lhs: &Self::Element, rhs: &Self::Element) -> Self::Element {
// this is just for demonstration purposes - note that the length of the vectors would slowly increase,
// even if the degree of the polynomials doesn't
let mut result = (0..(lhs.len() + rhs.len())).map(|_| self.base_ring.zero()).collect::<Vec<_>>();
for i in 0..lhs.len() {
for j in 0..rhs.len() {
self.base_ring.add_assign(&mut result[i + j], self.base_ring.mul_ref(&lhs[i], &rhs[j]));
}
}
return result;
}
fn mul_assign(&self, lhs: &mut Self::Element, rhs: Self::Element) {
*lhs = self.mul_ref(lhs, &rhs);
}
fn from_int(&self, x: i32) -> Self::Element {
vec![self.base_ring.int_hom().map(x)]
}
fn eq_el(&self, lhs: &Self::Element, rhs: &Self::Element) -> bool {
let zero = self.base_ring.zero();
for i in 0..max(lhs.len(), rhs.len()) {
if !self.base_ring.eq_el(lhs.get(i).unwrap_or(&zero), rhs.get(i).unwrap_or(&zero)) {
return false;
}
}
return true;
}
fn is_commutative(&self) -> bool {
self.base_ring.is_commutative()
}
fn is_noetherian(&self) -> bool {
// by Hilbert's basis theorem
self.base_ring.is_noetherian()
}
fn dbg(&self, val: &Self::Element, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
// this is just for demonstration purposes - note that this prints zero coefficients
for i in 0..(val.len() - 1) {
write!(f, "{} * X^{} + ", self.base_ring.format(&val[i]), i)?;
}
write!(f, "{} * X^{}", self.base_ring.format(val.last().unwrap()), val.len() - 1)
}
fn characteristic<I>(&self, ZZ: &I) -> Option<El<I>>
where I: IntegerRingStore, I::Type: IntegerRing
{
self.base_ring.get_ring().characteristic(ZZ)
}
}
// in a real implementation, we definitely should implement also `feanor_math::rings::poly::PolyRing`, and
// possibly other traits (`CanHomFrom<other polynomial rings>`, `RingExtension`, `DivisibilityRing`, `EuclideanRing`)
let base = zn_static::F17;
// we do not use the `RingBase`-implementor directly, but wrap it in a `RingStore`;
// note that here, this is not strictly necessary, but still recommended
let ring = RingValue::from(MyPolyRing { base_ring: base });
let x = vec![0, 1];
let f = ring.add_ref(&x, &ring.int_hom().map(8));
let g = ring.add_ref(&x, &ring.int_hom().map(7));
let h = ring.add(ring.mul_ref(&x, &x), ring.add_ref(&ring.mul_ref(&x, &ring.int_hom().map(-2)), &ring.int_hom().map(5)));
assert_el_eq!(ring, h, ring.mul(f, g));
RingBase
与 RingStore
特质 RingBase
的设计是为了提供一个简单的方式来定义环结构。因此,它提供了一个具有所有环操作(如加法、乘法和等式测试)的基本接口。在许多情况下,定义了基本操作的变体以利用移动语义和内存重用。然而,它们通常具有默认实现,以简化创建新的环。
另一方面,RingStore
是任何可以访问底层 RingBase
对象的对象。这个特质提供了所有环操作的默认实现,这些实现只是将调用委托给底层 RingBase
对象。在正常情况下,这个特质不应为自定义类型实现。
当我们开始嵌套环时,这种分离的主要优势变得明显。比如说,我们有一个基于底层环类型的环类型,例如在基环 R
上的多项式环 PolyRing<R>
。在这种情况下,PolyRing
实现 RingBase
,但底层环 R
被限制为必须是 RingStore
。因此,类型如 PolyRing<R>
、PolyRing<&&R>
和 PolyRing<Box<R>>
都可以等价使用,这提供了很大的灵活性,但仍然可以与昂贵的克隆环和零大小环一起工作。
约定和最佳实践
- 许多以环为参数的函数目前使用通用类型
RingStore
的R
类型作为&R
。然而,这并不是最佳选择,相反,应该选择通过值传递环,即R
。如果需要在函数内部复制环,则完全允许要求R: Copy + RingStore
。因为对于R: RingStore
同样有&R: RingStore
,这使得接口更加通用。 - 封装基本环的环(如
MyRing<BaseRing: RingStore>
)不应该实现Copy
,除非BaseRing: Copy
和El<BaseRing>: Copy
都实现。在某些情况下,我已经添加了#[derive(Copy)]
(例如feanor_math::rings::rational::RationalFieldBase
),现在这可以阻止将基本环元素的成员添加到环中。特别是,这样做意味着如果只有基本环而没有其元素实现Copy
,则无法实现Copy
,从而破坏向后兼容性。在下一个重大版本更新中,这将进行更改。
性能
总的来说,我希望在这个库中将性能作为一个高优先级。然而,到目前为止,我没有时间彻底优化许多算法。
实现最佳性能的建议
- 在项目的
Cargo.toml
中使用lto = "fat"
。这对于在库边界之间启用内联至关重要,如果广泛使用具有“简单”基本算术的环(如zn_64::Zn
或primitive_int::StaticRing
),则可以产生巨大影响。 - 这个库的不同部分处于不同的优化阶段。虽然我在有限域和FFT算法上花了一些时间,但例如整数分解目前相对较慢。
- 如果您广泛使用需要动态内存分配的环元素,请务必使用自定义分配器,例如来自
feanor-mempool
的一个。 - 默认的任意精度整数算术目前较慢。如果您大量使用任意精度整数,请使用功能“mpir”以及 mpir 库的安装。
设计决策
在这里,我记录了一些更重要设计决策——主要为了我自己。
RingStore
已经讨论过这个问题了。主要,这是我在第一个版本的这个crate中因为尝试将 RingWrapper<Zn<IntRing>>
的元素映射到 RingWrapper<Zn<&IntRing>>
或类似结构时,产生的头痛问题的结果。
引用环的元素
看起来这是一个相当常见的需求,即环的元素可能包含对环的引用。例如,如果元素使用由环的内存池管理的内存,那么这种情况就会发生。换句话说,我们会定义 RingBase
为
trait RingBase {
type Element<'a> where Self: 'a;
...
}
然而,这与另一个设计决策相冲突:我们希望能够嵌套环,并允许嵌套的环拥有所有权(而不仅仅是借用)。如果我们允许环引用元素,这现在将阻止我们定义存储嵌套环及其一些元素的环。例如,Z/qZ 的实现可能看起来像
struct Zn<I: IntegerRingStore> {
integer_ring: I,
modulus: El<I>
}
如果 El<I>
可能包含对 I
的引用,那么这个结构就是自引用的,会引起很多麻烦。
现在看来,禁止拥有嵌套环而不是环引用元素似乎更自然,但这将严重限制可以从函数返回的环。例如,我们可能希望一个函数能够生成具有基于内存池的大整数实现如 Fq 的函数
fn galois_field(q: i64, exponent: usize) -> RingExtension<ZnBarett<MempoolBigIntRing>> {
...
}
这将是不可能的,以及许多其他用例。另一方面,如果我们想要在静态生命周期分析的情况下执行运行时检查,那么在环引用元素的情况下,这会更简单。这可能稍微有些不自然,但非常实用。实际上,如果元素需要引用环,那么它们不会很小,并且算术成本将远远超过运行时管理成本。
依赖关系
~0.4–1.5MB
~31K SLoC