在python / numpy中编写蒙特卡罗粒子模拟器(布朗运动和光子发射)。我需要将模拟输出(>> 10GB)保存到文件中,然后在第二步中处理数据。与Windows和Linux的兼容性非常重要。
粒子数(n_particles
)为10-100。时间步数(time_size
)为~10 ^ 9。
模拟有3个步骤(下面的代码适用于全内存版本):
模拟(并存储) emission
费率数组(包含许多近0个元素):
n_particles
x time_size
),float32,尺寸 80GB 计算 counts
数组,(来自泊松过程的随机值,具有先前计算的费率):
形状(n_particles
x time_size
),uint8,尺寸 20GB
counts = np.random.poisson(lam=emission).astype(np.uint8)
查找计数的时间戳(或索引)。计数几乎总是0,因此时间戳数组将适合RAM。
# Loop across the particles
timestamps = [np.nonzero(c) for c in counts]
我做了一次步骤1,然后重复步骤2-3很多次(~100次)。在将来,我可能需要在计算 emission
之前预先处理 cumsum
(应用counts
或其他功能)。
我有一个内存实现工作,我正在努力了解实现可以扩展到(更长)模拟的核外版本的最佳方法。
我需要将数组保存到文件中,我想使用单个文件进行模拟。我还需要一种“简单”的方式来存储和调用模拟参数字典(标量)。
理想情况下,我想要一个文件支持的numpy数组,我可以预先分配并填充块。然后,我希望numpy数组方法(max
,cumsum
,...)透明地工作,只需要一个chunksize
关键字来指定每个加载多少个数组迭代。
更好的是,我希望 Numexpr 不在缓存和RAM之间运行,而是在RAM和硬盘之间运行。
作为第一选择 我开始尝试使用pyTables,但我对它的复杂性和抽象(与numpy不同)不满意。此外,我目前的解决方案(如下所示)是UGLY而且效率不高。
所以我寻求答案的选择是
实现具有所需功能的numpy数组(如何?)
以更智能的方式使用pytable(不同的数据结构/方法)
使用另一个库:h5py,blaze,pandas ......(到目前为止我还没有尝试过任何一个)。
我将模拟参数保存在'/parameters'
组中:每个参数都转换为numpy数组标量。详细的解决方案,但它的工作原理。
我将emission
保存为可扩展数组(EArray
),因为我以块的形式生成数据,我需要追加每个新块(我知道最终的大小)。保存counts
更有问题。如果将其保存为pytable数组,则很难执行“count> = 2”之类的查询。因此,我将计数保存为多个表(每个粒子一个)[UGLY],我用.get_where_list('counts >= 2')
查询。我不确定这是否节省空间,并且
生成所有这些表而不是使用单个数组,显着地破坏了HDF5文件。而且,奇怪的是,创建这些表需要创建自定义dtype(即使是标准的numpy dtypes):
dt = np.dtype([('counts', 'u1')])
for ip in xrange(n_particles):
name = "particle_%d" % ip
data_file.create_table(
group, name, description=dt, chunkshape=chunksize,
expectedrows=time_size,
title='Binned timetrace of emitted ph (bin = t_step)'
' - particle_%d' % particle)
每个粒子计数“表”都有一个不同的名称(name = "particle_%d" % ip
),我需要将它们放在python列表中以便于迭代。
编辑:此问题的结果是名为PyBroMo的布朗运动模拟器。
答案 0 :(得分:3)
Dask.array可以在磁盘阵列(如PyTables或h5py)上执行max
,cumsum
等分块操作。
import h5py
d = h5py.File('myfile.hdf5')['/data']
import dask.array as da
x = da.from_array(d, chunks=(1000, 1000))
X外观和感觉就像一个numpy数组并复制了大部分API。 x上的操作将创建一个内存中操作的DAG,可以根据需要使用从磁盘流式传输多个内核来高效执行
da.exp(x).mean(axis=0).compute()
http://dask.pydata.org/en/latest/
conda install dask
or
pip install dask
答案 1 :(得分:2)
有关如何将参数存储在HDF5文件中的信息,请参阅here(它会发泡,因此您可以存储它们的方式;它们对泡菜的大小限制为64kb)。
import pandas as pd
import numpy as np
n_particles = 10
chunk_size = 1000
# 1) create a new emission file, compressing as we go
emission = pd.HDFStore('emission.hdf',mode='w',complib='blosc')
# generate simulated data
for i in range(10):
df = pd.DataFrame(np.abs(np.random.randn(chunk_size,n_particles)),dtype='float32')
# create a globally unique index (time)
# http://stackoverflow.com/questions/16997048/how-does-one-append-large-amounts-of-
data-to-a-pandas-hdfstore-and-get-a-natural/16999397#16999397
try:
nrows = emission.get_storer('df').nrows
except:
nrows = 0
df.index = pd.Series(df.index) + nrows
emission.append('df',df)
emission.close()
# 2) create counts
cs = pd.HDFStore('counts.hdf',mode='w',complib='blosc')
# this is an iterator, can be any size
for df in pd.read_hdf('emission.hdf','df',chunksize=200):
counts = pd.DataFrame(np.random.poisson(lam=df).astype(np.uint8))
# set the index as the same
counts.index = df.index
# store the sum across all particles (as most are zero this will be a
# nice sub-selector
# better maybe to have multiple of these sums that divide the particle space
# you don't have to do this but prob more efficient
# you can do this in another file if you want/need
counts['particles_0_4'] = counts.iloc[:,0:4].sum(1)
counts['particles_5_9'] = counts.iloc[:,5:9].sum(1)
# make the non_zero column indexable
cs.append('df',counts,data_columns=['particles_0_4','particles_5_9'])
cs.close()
# 3) find interesting counts
print pd.read_hdf('counts.hdf','df',where='particles_0_4>0')
print pd.read_hdf('counts.hdf','df',where='particles_5_9>0')
您也可以将每个粒子设为data_column并单独选择它们。
和一些输出(在这种情况下相当活跃的发射:)
0 1 2 3 4 5 6 7 8 9 particles_0_4 particles_5_9
0 2 2 2 3 2 1 0 2 1 0 9 4
1 1 0 0 0 1 0 1 0 3 0 1 4
2 0 2 0 0 2 0 0 1 2 0 2 3
3 0 0 0 1 1 0 0 2 0 3 1 2
4 3 1 0 2 1 0 0 1 0 0 6 1
5 1 0 0 1 0 0 0 3 0 0 2 3
6 0 0 0 1 1 0 1 0 0 0 1 1
7 0 2 0 2 0 0 0 0 2 0 4 2
8 0 0 0 1 3 0 0 0 0 1 1 0
10 1 0 0 0 0 0 0 0 0 1 1 0
11 0 0 1 1 0 2 0 1 2 1 2 5
12 0 2 2 4 0 0 1 1 0 1 8 2
13 0 2 1 0 0 0 0 1 1 0 3 2
14 1 0 0 0 0 3 0 0 0 0 1 3
15 0 0 0 1 1 0 0 0 0 0 1 0
16 0 0 0 4 3 0 3 0 1 0 4 4
17 0 2 2 3 0 0 2 2 0 2 7 4
18 0 1 2 1 0 0 3 2 1 2 4 6
19 1 1 0 0 0 0 1 2 1 1 2 4
20 0 0 2 1 2 2 1 0 0 1 3 3
22 0 1 2 2 0 0 0 0 1 0 5 1
23 0 2 4 1 0 1 2 0 0 2 7 3
24 1 1 1 0 1 0 0 1 2 0 3 3
26 1 3 0 4 1 0 0 0 2 1 8 2
27 0 1 1 4 0 1 2 0 0 0 6 3
28 0 1 0 0 0 0 0 0 0 0 1 0
29 0 2 0 0 1 0 1 0 0 0 2 1
30 0 1 0 2 1 2 0 2 1 1 3 5
31 0 0 1 1 1 1 1 0 1 1 2 3
32 3 0 2 1 0 0 1 0 1 0 6 2
33 1 3 1 0 4 1 1 0 1 4 5 3
34 1 1 0 0 0 0 0 3 0 1 2 3
35 0 1 0 0 1 1 2 0 1 0 1 4
36 1 0 1 0 1 2 1 2 0 1 2 5
37 0 0 0 1 0 0 0 0 3 0 1 3
38 2 5 0 0 0 3 0 1 0 0 7 4
39 1 0 0 2 1 1 3 0 0 1 3 4
40 0 1 0 0 1 0 0 4 2 2 1 6
41 0 3 3 1 1 2 0 0 2 0 7 4
42 0 1 0 2 0 0 0 0 0 1 3 0
43 0 0 2 0 5 0 3 2 1 1 2 6
44 0 2 0 1 0 0 1 0 0 0 3 1
45 1 0 0 2 0 0 0 1 4 0 3 5
46 0 2 0 0 0 0 0 1 1 0 2 2
48 3 0 0 0 0 1 1 0 0 0 3 2
50 0 1 0 1 0 1 0 0 2 1 2 3
51 0 0 2 0 0 0 2 3 1 1 2 6
52 0 0 2 3 2 3 1 0 1 5 5 5
53 0 0 0 2 1 1 0 0 1 1 2 2
54 0 1 2 2 2 0 1 0 2 0 5 3
55 0 2 1 0 0 0 0 0 3 2 3 3
56 0 1 0 0 0 2 2 0 1 1 1 5
57 0 0 0 1 1 0 0 1 0 0 1 1
58 6 1 2 0 2 2 0 0 0 0 9 2
59 0 1 1 0 0 0 0 0 2 0 2 2
60 2 0 0 0 1 0 0 1 0 1 2 1
61 0 0 3 1 1 2 0 0 1 0 4 3
62 2 0 1 0 0 0 0 1 2 1 3 3
63 2 0 1 0 1 0 1 0 0 0 3 1
65 0 0 1 0 0 0 1 5 0 1 1 6
.. .. .. .. .. .. .. .. .. .. ... ...
[9269 rows x 12 columns]
答案 2 :(得分:2)
由于不需要Pandas提供的功能,并且处理速度要慢得多(请参阅下面的笔记本),最好的方法是直接使用PyTables或h5py。到目前为止,我只尝试过pytables方法。
所有测试均在此笔记本中进行:
Pytables允许以两种格式存储HDF5文件中的数据:数组和表格。
有三种类型的数组Array
,CArray
和EArray
。它们都允许存储和检索(多维)切片,其符号类似于numpy切片。
# Write data to store (broadcasting works)
array1[:] = 3
# Read data from store
in_ram_array = array1[:]
为了在某些用例中进行优化,CArray
保存在“块”中,其大小可以在创建时使用chunk_shape
进行选择。
Array
和CArray
大小在创建时是固定的。您可以在创建后填充/写入数组块。相反,EArray
可以使用.append()
方法进行扩展。
table
是一个完全不同的野兽。它基本上是一个“桌子”。您只有1D索引,每个元素都是一行。每行内部都有“列”数据类型,每列可以有不同的类型。你熟悉numpy record-arrays,一个表基本上是一个1D记录数组,每个元素都有很多字段作为列。
1D或2D numpy数组可以存储在表中,但它有点棘手:我们需要创建一个行数据类型。例如,要存储我们需要执行的1D uint8 numpy数组:
table_uint8 = np.dtype([('field1', 'u1')])
table_1d = data_file.create_table('/', 'array_1d', description=table_uint8)
那么为什么要使用桌子呢?因为,与数组不同,可以有效地查询表。例如,如果我们想要搜索元素>我们可以在一个巨大的基于磁盘的表中执行3:
index = table_1d.get_where_list('field1 > 3')
不仅简单(与我们需要以块的形式扫描整个文件并在循环中构建index
的数组相比),但它也非常快。
存储模拟参数的最佳方法是使用一个组(即/parameters
),将每个标量转换为numpy数组并将其存储为CArray
。
emission
” emission
是按顺序生成和读取的最大数组。对于此使用模式,良好的数据结构为EArray
。在“模拟”数据上,约有50%的零元素 blosc 压缩(level=5
)达到2.2倍压缩比。我发现大小为2 ^ 18(256k)的处理时间最短。
counts
”同时存储“counts
”会使文件大小增加10%,并且计算时间戳的时间会增加40%。存储counts
并不是一个优势,因为最终只需要时间戳。
优点是重新构建索引(时间戳)更简单,因为我们在单个命令(.get_where_list('counts >= 1')
)中查询全时轴。相反,对于分块处理,我们需要执行一些有点棘手的索引算法,并且可能是维护的负担。
然而,与两种情况下所需的所有其他操作(排序和合并)相比,代码复杂性可能很小。
timestamps
”时间戳可以在RAM中累积。但是,我们在启动之前不知道数组大小,并且需要进行最后hstack()
调用以“合并”存储在列表中的不同块。这使内存需求翻倍,因此RAM可能不足。
我们可以使用.append()
将as-we-go时间戳存储到表中。最后,我们可以使用.read()
将表加载到内存中。这比全内存计算慢10%,但避免了“双RAM”要求。此外,我们可以避免最终的满载并且RAM使用率最低。
H5py 是一个比 pytables 简单得多的库。对于这种(主要)顺序处理的用例似乎比pytables更合适。唯一缺少的功能是缺乏'blosc'压缩。如果这导致大的性能损失仍有待测试。
答案 3 :(得分:0)
使用OpenMM模拟粒子(https://github.com/SimTk/openmm)和MDTraj(https://github.com/rmcgibbo/mdtraj)来处理轨迹IO。
答案 4 :(得分:0)
接受的答案中的pytables vs pandas.HDFStore
测试完全是误导性的:
第一个严重错误是时间不适用os.fsync
之后
冲洗,这使速度测试不稳定。所以有时候,pytables
功能意外快得多。
第二个问题是pytables和pandas版本完全没有
由于误解pytables.EArray
而导致的形状不同
形状arg。作者尝试将列附加到pytables版本中,但是
将行添加到pandas版本中。
第三个问题是作者在期间使用了不同的chunkshape
比较。
作者也忘记在store.append()
期间禁用表格索引生成,这是一个耗时的过程。
下表显示了他的版本和我的修复程序的性能结果。
tbold
是他的pytables版本,pdold
是他的pandas版本。 tbsync
和pdsync
是fsync()
之后的flush()
的版本,并且还会在追加期间禁用表格索引生成。 tbopt
和pdopt
是我的优化版本,blosc:lz4
和补充9。
| name | dt | data size [MB] | comp ratio % | chunkshape | shape | clib | indexed |
|:-------|-----:|-----------------:|---------------:|:-------------|:--------------|:----------------|:----------|
| tbold | 5.11 | 300.00 | 84.63 | (15, 262144) | (15, 5242880) | blosc[5][1] | False |
| pdold | 8.39 | 340.00 | 39.26 | (1927,) | (5242880,) | blosc[5][1] | True |
| tbsync | 7.47 | 300.00 | 84.63 | (15, 262144) | (15, 5242880) | blosc[5][1] | False |
| pdsync | 6.97 | 340.00 | 39.27 | (1927,) | (5242880,) | blosc[5][1] | False |
| tbopt | 4.78 | 300.00 | 43.07 | (4369, 15) | (5242880, 15) | blosc:lz4[9][1] | False |
| pdopt | 5.73 | 340.00 | 38.53 | (3855,) | (5242880,) | blosc:lz4[9][1] | False |
pandas.HDFStore
使用了pytables
。因此,如果我们正确使用它们,它们应该没有任何区别。
我们可以看到pandas版本的数据量更大。这是因为pandas使用pytables.Table而不是EArray。并且pandas.DataFrame始终具有索引列。 Table对象的第一列是这个DataFrame索引,需要一些额外的空间来保存。这只会略微影响IO性能,但会提供更多功能,例如核外查询。所以我仍然在这里推荐大熊猫。 @MRocklin还提到了一个更好的核外包dask,如果你使用的大多数功能只是数组操作而不是类似于表的查询。但IO性能不会有明显区别。
h5f.root.emission._v_attrs
Out[82]:
/emission._v_attrs (AttributeSet), 15 attributes:
[CLASS := 'GROUP',
TITLE := '',
VERSION := '1.0',
data_columns := [],
encoding := 'UTF-8',
index_cols := [(0, 'index')],
info := {1: {'names': [None], 'type': 'RangeIndex'}, 'index': {}},
levels := 1,
metadata := [],
nan_rep := 'nan',
non_index_axes := [(1, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])],
pandas_type := 'frame_table',
pandas_version := '0.15.2',
table_type := 'appendable_frame',
values_cols := ['values_block_0']]
这是功能:
def generate_emission(shape):
"""Generate fake emission."""
emission = np.random.randn(*shape).astype('float32') - 1
emission.clip(0, 1e6, out=emission)
assert (emission >=0).all()
return emission
def test_puretb_earray(outpath,
n_particles = 15,
time_chunk_size = 2**18,
n_iter = 20,
sync = True,
clib = 'blosc',
clevel = 5,
):
time_size = n_iter * time_chunk_size
data_file = pytb.open_file(outpath, mode="w")
comp_filter = pytb.Filters(complib=clib, complevel=clevel)
emission = data_file.create_earray('/', 'emission', atom=pytb.Float32Atom(),
shape=(n_particles, 0),
chunkshape=(n_particles, time_chunk_size),
expectedrows=n_iter * time_chunk_size,
filters=comp_filter)
# generate simulated emission data
t0 =time()
for i in range(n_iter):
emission_chunk = generate_emission((n_particles, time_chunk_size))
emission.append(emission_chunk)
emission.flush()
if sync:
os.fsync(data_file.fileno())
data_file.close()
t1 = time()
return t1-t0
def test_puretb_earray2(outpath,
n_particles = 15,
time_chunk_size = 2**18,
n_iter = 20,
sync = True,
clib = 'blosc',
clevel = 5,
):
time_size = n_iter * time_chunk_size
data_file = pytb.open_file(outpath, mode="w")
comp_filter = pytb.Filters(complib=clib, complevel=clevel)
emission = data_file.create_earray('/', 'emission', atom=pytb.Float32Atom(),
shape=(0, n_particles),
expectedrows=time_size,
filters=comp_filter)
# generate simulated emission data
t0 =time()
for i in range(n_iter):
emission_chunk = generate_emission((time_chunk_size, n_particles))
emission.append(emission_chunk)
emission.flush()
if sync:
os.fsync(data_file.fileno())
data_file.close()
t1 = time()
return t1-t0
def test_purepd_df(outpath,
n_particles = 15,
time_chunk_size = 2**18,
n_iter = 20,
sync = True,
clib='blosc',
clevel=5,
autocshape=False,
oldversion=False,
):
time_size = n_iter * time_chunk_size
emission = pd.HDFStore(outpath, mode='w', complib=clib, complevel=clevel)
# generate simulated data
t0 =time()
for i in range(n_iter):
# Generate fake emission
emission_chunk = generate_emission((time_chunk_size, n_particles))
df = pd.DataFrame(emission_chunk, dtype='float32')
# create a globally unique index (time)
# http://stackoverflow.com/questions/16997048/how-does-one-append-large-
# amounts-of-data-to-a-pandas-hdfstore-and-get-a-natural/16999397#16999397
try:
nrows = emission.get_storer('emission').nrows
except:
nrows = 0
df.index = pd.Series(df.index) + nrows
if autocshape:
emission.append('emission', df, index=False,
expectedrows=time_size
)
else:
if oldversion:
emission.append('emission', df)
else:
emission.append('emission', df, index=False)
emission.flush(fsync=sync)
emission.close()
t1 = time()
return t1-t0
def _test_puretb_earray_nosync(outpath):
return test_puretb_earray(outpath, sync=False)
def _test_purepd_df_nosync(outpath):
return test_purepd_df(outpath, sync=False,
oldversion=True
)
def _test_puretb_earray_opt(outpath):
return test_puretb_earray2(outpath,
sync=False,
clib='blosc:lz4',
clevel=9
)
def _test_purepd_df_opt(outpath):
return test_purepd_df(outpath,
sync=False,
clib='blosc:lz4',
clevel=9,
autocshape=True
)
testfns = {
'tbold':_test_puretb_earray_nosync,
'pdold':_test_purepd_df_nosync,
'tbsync':test_puretb_earray,
'pdsync':test_purepd_df,
'tbopt': _test_puretb_earray_opt,
'pdopt': _test_purepd_df_opt,
}