Python粒子模拟器:核外处理

时间:2014-01-05 23:55:16

标签: numpy pandas pytables h5py blaze

问题描述

在python / numpy中编写蒙特卡罗粒子模拟器(布朗运动和光子发射)。我需要将模拟输出(>> 10GB)保存到文件中,然后在第二步中处理数据。与Windows和Linux的兼容性非常重要。

粒子数(n_particles)为10-100。时间步数(time_size)为~10 ^ 9。

模拟有3个步骤(下面的代码适用于全内存版本):

  1. 模拟(并存储) emission 费率数组(包含许多近0个元素):

    • 形状(n_particles x time_size),float32,尺寸 80GB
  2. 计算 counts 数组,(来自泊松过程的随机值,具有先前计算的费率):

    • 形状(n_particles x time_size),uint8,尺寸 20GB

      counts = np.random.poisson(lam=emission).astype(np.uint8)
      
  3. 查找计数的时间戳(或索引)。计数几乎总是0,因此时间戳数组将适合RAM。

    # Loop across the particles
    timestamps = [np.nonzero(c) for c in counts]
    
  4. 我做了一次步骤1,然后重复步骤2-3很多次(~100次)。在将来,我可能需要在计算 emission 之前预先处理 cumsum (应用counts或其他功能)。

    问题

    我有一个内存实现工作,我正在努力了解实现可以扩展到(更长)模拟的核外版本的最佳方法。

    我希望它存在

    我需要将数组保存到文件中,我想使用单个文件进行模拟。我还需要一种“简单”的方式来存储和调用模拟参数字典(标量)。

    理想情况下,我想要一个文件支持的numpy数组,我可以预先分配并填充块。然后,我希望numpy数组方法(maxcumsum,...)透明地工作,只需要一个chunksize关键字来指定每个加载多少个数组迭代。

    更好的是,我希望 Numexpr 不在缓存和RAM之间运行,而是在RAM和硬盘之间运行。

    有哪些实用选择

    作为第一选择 我开始尝试使用pyTables,但我对它的复杂性和抽象(与numpy不同)不满意。此外,我目前的解决方案(如下所示)是UGLY而且效率不高。

    所以我寻求答案的选择是

    1. 实现具有所需功能的numpy数组(如何?)

    2. 以更智能的方式使用pytable(不同的数据结构/方法)

    3. 使用另一个库:h5py,blaze,pandas ......(到目前为止我还没有尝试过任何一个)。

    4. 暂定解决方案(pyTables)

      我将模拟参数保存在'/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的布朗运动模拟器。

5 个答案:

答案 0 :(得分:3)

Dask.array可以在磁盘阵列(如PyTables或h5py)上执行maxcumsum等分块操作。

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)

PyTable Solution

由于不需要Pandas提供的功能,并且处理速度要慢得多(请参阅下面的笔记本),最好的方法是直接使用PyTables或h5py。到目前为止,我只尝试过pytables方法。

所有测试均在此笔记本中进行:

pytables数据结构简介

  

参考:Official PyTables Docs

Pytables允许以两种格式存储HDF5文件中的数据:数组和表格。

阵列

有三种类型的数组ArrayCArrayEArray。它们都允许存储和检索(多维)切片,其符号类似于numpy切片。

# Write data to store (broadcasting works)
array1[:]  = 3

# Read data from store
in_ram_array = array1[:]

为了在某些用例中进行优化,CArray保存在“块”中,其大小可以在创建时使用chunk_shape进行选择。

ArrayCArray大小在创建时是固定的。您可以在创建后填充/写入数组块。相反,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

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版本。 tbsyncpdsyncfsync()之后的flush()的版本,并且还会在追加期间禁用表格索引生成。 tboptpdopt是我的优化版本,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,
}