Python 高效构造有限元/有限体积法矩阵

Python 高效构造有限元/有限体积法矩阵,python,numpy,matrix,scipy,scientific-computing,Python,Numpy,Matrix,Scipy,Scientific Computing,这是FEM/FVM方程系统的典型用例,因此可能具有更广泛的兴趣。从一个三角形网格开始 我想创建一个scipy.sparse.csr\u矩阵。矩阵行/列表示网格节点处的值。矩阵在主对角线上以及两个节点通过边连接的任何位置都有条目 以下是一个MWE,它首先构建节点->边缘->单元关系,然后构建矩阵: import numpy import meshzoo from scipy import sparse nx = 1600 ny = 1000 verts, cells = meshzoo.rec

这是FEM/FVM方程系统的典型用例,因此可能具有更广泛的兴趣。从一个三角形网格开始

我想创建一个
scipy.sparse.csr\u矩阵
。矩阵行/列表示网格节点处的值。矩阵在主对角线上以及两个节点通过边连接的任何位置都有条目

以下是一个MWE,它首先构建节点->边缘->单元关系,然后构建矩阵:

import numpy
import meshzoo
from scipy import sparse

nx = 1600
ny = 1000
verts, cells = meshzoo.rectangle(0.0, 1.61, 0.0, 1.0, nx, ny)

n = len(verts)

nds = cells.T
nodes_edge_cells = numpy.stack([nds[[1, 2]], nds[[2, 0]],nds[[0, 1]]], axis=1)

# assign values to each edge (per cell)
alpha = numpy.random.rand(3, len(cells))
vals = numpy.array([
    [alpha**2, -alpha],
    [-alpha, alpha**2],
    ])

# Build I, J, V entries for COO matrix
I = []
J = []
V = []
#
V.append(vals[0][0])
V.append(vals[0][1])
V.append(vals[1][0])
V.append(vals[1][1])
#
I.append(nodes_edge_cells[0])
I.append(nodes_edge_cells[0])
I.append(nodes_edge_cells[1])
I.append(nodes_edge_cells[1])
#
J.append(nodes_edge_cells[0])
J.append(nodes_edge_cells[1])
J.append(nodes_edge_cells[0])
J.append(nodes_edge_cells[1])
# Create suitable data for coo_matrix
I = numpy.concatenate(I).flat
J = numpy.concatenate(J).flat
V = numpy.concatenate(V).flat

matrix = sparse.coo_matrix((V, (I, J)), shape=(n, n))
matrix = matrix.tocsr()

您可以创建并查看上述配置文件:

方法
tocsr()。因此,我正在寻找加快速度的方法

我已经发现:

  • 由于数据的结构,矩阵对角线上的值可以提前求和,即:

    V.append(vals[0, 0, 0] + vals[1, 1, 2])
    I.append(nodes_edge_cells[0, 0])  # == nodes_edge_cells[1, 2]
    J.append(nodes_edge_cells[0, 0])  # == nodes_edge_cells[1, 2]
    
    这使得
    I
    J
    V
    更短,从而加快了
    tocsr

  • 现在,边是“每个单元”。我可以使用
    numpy.unique
    识别彼此相等的边,有效地节省了
    I
    J
    V
    的一半。然而,我发现这也需要一些时间。(这并不奇怪。)

我的另一个想法是我可以用一个简单的
numpy.add.at
替换对角线
V
I
J
,如果有一个类似
csr\u矩阵的数据结构,其中主对角线是分开保存的。我知道这存在于其他一些软件包中,但在scipy中找不到。对吗


也许有一种合理的方法可以直接构建CSR?

我会尝试直接创建CSR结构,特别是如果您使用
np.unique
,因为这会为您提供排序键,这是完成工作的一半

我假设您正处于这样一个点上,即您使用
np.unique
的可选
inverse
输出上的
I,j
按字典顺序排序并重叠
v
求和
np.add.at

然后,
v
j
已采用csr格式。剩下要做的就是创建
indptr
,您只需通过
np.searchsorted(i,np.arange(M+1))
即可获得它,其中
M
是列长度。您可以将它们直接传递给
sparse.csr\u矩阵
构造函数

好的,让代码说:

import numpy as np
from scipy import sparse
from timeit import timeit


def tocsr(I, J, E, N):
    n = len(I)
    K = np.empty((n,), dtype=np.int64)
    K.view(np.int32).reshape(n, 2).T[...] = J, I  
    S = np.argsort(K)
    KS = K[S]
    steps = np.flatnonzero(np.r_[1, np.diff(KS)])
    ED = np.add.reduceat(E[S], steps)
    JD, ID = KS[steps].view(np.int32).reshape(-1, 2).T
    ID = np.searchsorted(ID, np.arange(N+1))
    return sparse.csr_matrix((ED, np.array(JD, dtype=int), ID), (N, N))

def viacoo(I, J, E, N):
    return sparse.coo_matrix((E, (I, J)), (N, N)).tocsr()


#testing and timing

# correctness
N = 1000
A = np.random.random((N, N)) < 0.001
I, J = np.where(A)

E = np.random.random((2, len(I)))
D = np.zeros((2,) + A.shape)
D[:, I, J] = E
D2 = tocsr(np.r_[I, I], np.r_[J, J], E.ravel(), N).A

print('correct:', np.allclose(D.sum(axis=0), D2))

# speed
N = 100000
K = 10

I, J = np.random.randint(0, N, (2, K*N))
E = np.random.random((2 * len(I),))
I, J, E = np.r_[I, I, J, J], np.r_[J, J, I, I], np.r_[E, E]

print('N:', N, ' --  nnz (with duplicates):', len(E))
print('direct: ', timeit('f(a,b,c,d)', number=10, globals={'f': tocsr, 'a': I, 'b': J, 'c': E, 'd': N}), 'secs for 10 iterations')
print('via coo:', timeit('f(a,b,c,d)', number=10, globals={'f': viacoo, 'a': I, 'b': J, 'c': E, 'd': N}), 'secs for 10 iterations')

加速比:5x

因此,最终证明这是COO和CSR的
sum_duplicates
(就像@hpaulj怀疑的那样)之间的差异。多亏了这里的每个人(特别是@paul panzer)的努力,我们正在进行
tocsr
的巨大加速

SciPy的
tocsr
(I,J)
进行了
lexsort
,因此它有助于组织索引,使
(I,J)
能够得到相当的排序

对于上述示例中的
nx=4
ny=2
I
J

[1 6 3 5 2 7 5 5 7 4 5 6 0 2 2 0 1 2 1 6 3 5 2 7 5 5 7 4 5 6 0 2 2 0 1 2 5 5 7 4 5 6 0 2 2 0 1 2 1 6 3 5 2 7 5 5 7 4 5 6 0 2 2 0 1 2 1 6 3 5 2 7]
[1 6 3 5 2 7 5 5 7 4 5 6 0 2 2 0 1 2 5 5 7 4 5 6 0 2 2 0 1 2 1 6 3 5 2 7 1 6 3 5 2 7 5 5 7 4 5 6 0 2 2 0 1 2 5 5 7 4 5 6 0 2 2 0 1 2 1 6 3 5 2 7]
首先对
单元格的每一行进行排序,然后按第一列对行进行排序,如

cells = numpy.sort(cells, axis=1)
cells = cells[cells[:, 0].argsort()]
产生

[1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6]
[1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6]
对于原始帖子中的数字,排序将运行时间从大约40秒缩短到8秒


如果首先对节点进行更适当的编号,也许可以实现更好的排序。我在考虑和。

我想看看不同步骤的时间安排。
tocsr
使用编译代码。我认为,在手之前对
coo
输入进行任何按摩都会花费同样长的时间,如果不是更多的话。你确定
tocsr
会花费很长时间吗?我对一个10k×10k的矩阵做了一些非常类似的事情,其中I,J,V的长度已经达到了数百万,而且没花那么长的时间。也许5-10秒。谢谢你的评论。我在原来的帖子中添加了个人资料。很有趣。我以为
sum\u duplicates
是编译的
csr
构造的一部分,但是您的配置文件显示它是
coo.py
中的Python方法。它使用
lexsort
np.nonzero
reduceat
。因此,做你自己的
重复计算
是有希望的。如果您的
coo
具有规范的格式
tocsr
应该快得多。
csr
有自己的
sum\u duplicates
方法。它使用编译的
sparse.\u sparsetools.csr\u sum\u duplicates
。我实际上希望避免使用
unique
,因为它不是一个O(n)操作(或者我认为是这样)。当然,不管怎样,如果
to_csr
做到了这一点,我也可以自己做。所有的提示都很好。我似乎记得,ber
unique
使用了
argsort
,所以它是O(n logn)。另一方面,CSR对索引进行了排序,因此除非您的数据以任何形式进行了预排序(是吗?),或者您有一些图论约束(例如,每个节点的边数固定或有界),否则无法避免O(n log n)。我不认为您的数据是按节点分组的还是类似的?网格是否与外观一样规则?因为可以利用这种结构来实现更快的排序。看来加快排序的关键是以一种最小化
lexsort
时间的方式生成元素。如果重复项彼此相邻,
add.reduceat
可以快速求和。@NicoSchlömer我已经实现了直接tocsr,看起来比通过coo要快得多。很可能大部分的加速是由于我避免使用
lexsort
而使用
argsort
。你能查一下你的实际数据吗?我很想知道它在现实世界中的效果有多好。@hpaulj似乎lexsort使用mergesort,而argsort在默认情况下使用quicksort。当我传递
kind=mergesort
时,argsort生成与lexsort相同的输出。运行速度慢了两倍多。
cells = numpy.sort(cells, axis=1)
cells = cells[cells[:, 0].argsort()]
[1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6]
[1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6 1 4 2 5 3 6 5 5 5 6 7 7 0 0 1 2 2 2 5 5 5 6 7 7 0 0 1 2 2 2 1 4 2 5 3 6]