diff options
| author | Stan Seibert <stan@mtrr.org> | 2011-10-07 15:51:47 -0400 | 
|---|---|---|
| committer | Stan Seibert <stan@mtrr.org> | 2011-10-07 15:51:47 -0400 | 
| commit | c13c687cc1c3fed4484be2a19fc045cee7b3d310 (patch) | |
| tree | d6f6d5a4d17021eb2b0270bb7d16af1a038e1be6 | |
| parent | d0a7ec169cbb3bf19024b6f9d337845e706b4fa2 (diff) | |
| download | chroma-c13c687cc1c3fed4484be2a19fc045cee7b3d310.tar.gz chroma-c13c687cc1c3fed4484be2a19fc045cee7b3d310.tar.bz2 chroma-c13c687cc1c3fed4484be2a19fc045cee7b3d310.zip  | |
Speed up Geometry.build() by a large factor when loading from cache.
A bunch of small tricks have been applied to reduce the amount of time
required to build an already cached geometry:
  * Replace uses of fromiter() on long sequences with code that operates
    on bigger arrays.
  * Use memoization on the Solids to more efficiently map materials
    to material codes when a solid is repeated (as is the case in all
    our detectors)
  * Use numpy.take() instead of fancy indexing on big arrays.  I
    learned about this trick from:
        http://wesmckinney.com/blog/?p=215
Also, switched over to compressed npz files for storing cache information.
They take the same size as the gzipped pickle files, but load 30% faster.
| -rw-r--r-- | chroma/geometry.py | 83 | ||||
| -rw-r--r-- | chroma/sim.py | 2 | ||||
| -rw-r--r-- | chroma/tools.py | 6 | 
3 files changed, 69 insertions, 22 deletions
diff --git a/chroma/geometry.py b/chroma/geometry.py index 8c97a74..80b83fc 100644 --- a/chroma/geometry.py +++ b/chroma/geometry.py @@ -7,7 +7,7 @@ import numpy as np  import time  from chroma.itertoolset import * -from chroma.tools import timeit, profile_if_possible +from chroma.tools import timeit, profile_if_possible, filled_array  from chroma.log import logger  # all material/surface properties are interpolated at these @@ -77,6 +77,31 @@ class Mesh(object):      def __add__(self, other):          return Mesh(np.concatenate((self.vertices, other.vertices)), np.concatenate((self.triangles, other.triangles + len(self.vertices)))) +def memoize_method_with_dictionary_arg(func): +    def lookup(*args): +        # based on function by Michele Simionato +        # http://www.phyast.pitt.edu/~micheles/python/ +        # Modified to work for class method with dictionary argument + +        assert len(args) == 2 +        # create hashable arguments by replacing dictionaries with tuples of items +        dict_items = args[1].items() +        dict_items.sort() +        hashable_args = (args[0], tuple(dict_items)) +        try: +            return func._memoize_dic[hashable_args] +        except AttributeError: +            # _memoize_dic doesn't exist yet. + +            result = func(*args) +            func._memoize_dic = {hashable_args: result} +            return result +        except KeyError: +            result = func(*args) +            func._memoize_dic[hashable_args] = result +            return result +    return lookup +  class Solid(object):      """Solid object attaches materials, surfaces, and colors to each triangle      in a Mesh object.""" @@ -119,6 +144,18 @@ class Solid(object):      def __add__(self, other):          return Solid(self.mesh + other.mesh, np.concatenate((self.material1, other.material1)), np.concatenate((self.material2, other.material2)), np.concatenate((self.surface, other.surface)), np.concatenate((self.color, other.color))) +    @memoize_method_with_dictionary_arg +    def material1_indices(self, material_lookup): +        return np.fromiter(imap(material_lookup.get, self.material1), dtype=np.int32, count=len(self.material1)) + +    @memoize_method_with_dictionary_arg +    def material2_indices(self, material_lookup): +        return np.fromiter(imap(material_lookup.get, self.material2), dtype=np.int32, count=len(self.material2)) + +    @memoize_method_with_dictionary_arg +    def surface_indices(self, surface_lookup): +        return np.fromiter(imap(surface_lookup.get, self.surface), dtype=np.int32, count=len(self.surface)) +  class Material(object):      """Material optical properties."""      def __init__(self, name='none'): @@ -247,6 +284,7 @@ class Geometry(object):          return len(self.solids)-1 +    @profile_if_possible      def build(self, bits=11, shift=3, use_cache=True):          """          Build the bounding volume hierarchy, material/surface code arrays, and @@ -285,25 +323,26 @@ class Geometry(object):                  np.inner(solid.mesh.vertices, self.solid_rotations[i]) + self.solid_displacements[i]              triangles[nt[i]:nt[i+1]] = solid.mesh.triangles + nv[i] -        self.mesh = Mesh(vertices, triangles) +        # Different solids are very unlikely to share vertices, so this goes much faster +        self.mesh = Mesh(vertices, triangles, remove_duplicate_vertices=False)          self.colors = np.concatenate([solid.color for solid in self.solids]) -        self.solid_id = np.concatenate([np.fromiter(repeat(i, len(solid.mesh.triangles)), dtype=np.uint32) for i, solid in enumerate(self.solids)]) +        self.solid_id = np.concatenate([filled_array(i, shape=len(solid.mesh.triangles), dtype=np.uint32) for i, solid in enumerate(self.solids)])          self.unique_materials = list(np.unique(np.concatenate([solid.unique_materials for solid in self.solids])))          material_lookup = dict(zip(self.unique_materials, range(len(self.unique_materials)))) -        self.material1_index = np.fromiter(imap(material_lookup.get, chain(*[solid.material1 for solid in self.solids])), dtype=np.int32) +        self.material1_index = np.concatenate([solid.material1_indices(material_lookup) for solid in self.solids]) -        self.material2_index = np.fromiter(imap(material_lookup.get, chain(*[solid.material2 for solid in self.solids])), dtype=np.int32) +        self.material2_index = np.concatenate([solid.material2_indices(material_lookup) for solid in self.solids])          self.unique_surfaces = list(np.unique(np.concatenate([solid.unique_surfaces for solid in self.solids])))          surface_lookup = dict(zip(self.unique_surfaces, range(len(self.unique_surfaces)))) -        self.surface_index = np.fromiter(imap(surface_lookup.get, chain(*[solid.surface for solid in self.solids])), dtype=np.int32) +        self.surface_index = np.concatenate([solid.surface_indices(surface_lookup) for solid in self.solids])          try:              self.surface_index[self.surface_index == surface_lookup[None]] = -1 @@ -316,29 +355,30 @@ class Geometry(object):          checksum.update(self.mesh.triangles)          cache_dir = os.path.expanduser('~/.chroma') -        cache_file = checksum.hexdigest() +        cache_file = checksum.hexdigest()+'.npz'          cache_path = os.path.join(cache_dir, cache_file)          if use_cache:              try: -                f = gzip.GzipFile(cache_path, 'rb') +                npz_file = np.load(cache_path)              except IOError:                  pass              else:                  logger.info('Loading BVH from cache.') -                data = pickle.load(f) +                data = dict(npz_file) +                # take() is faster than fancy indexing by 5x! +                # tip from http://wesmckinney.com/blog/?p=215                  reorder = data.pop('reorder') -                self.mesh.triangles = self.mesh.triangles[reorder] -                self.material1_index = self.material1_index[reorder] -                self.material2_index = self.material2_index[reorder] -                self.surface_index = self.surface_index[reorder] -                self.colors = self.colors[reorder] -                self.solid_id = self.solid_id[reorder] +                self.mesh.triangles = self.mesh.triangles.take(reorder, axis=0) +                self.material1_index = self.material1_index.take(reorder, axis=0) +                self.material2_index = self.material2_index.take(reorder, axis=0) +                self.surface_index = self.surface_index.take(reorder, axis=0) +                self.colors = self.colors.take(reorder, axis=0) +                self.solid_id = self.solid_id.take(reorder, axis=0)                  for key, value in data.iteritems():                      setattr(self, key, value) -                f.close()                  logger.info('  nodes: %d' % len(self.upper_bounds))                  return @@ -422,9 +462,8 @@ class Geometry(object):              if not os.path.exists(cache_dir):                  os.makedirs(cache_dir) -            with gzip.GzipFile(cache_path, 'wb', compresslevel=1) as f: -                data = {} -                for key in ['lower_bounds', 'upper_bounds', 'node_map', 'node_map_end', 'layers', 'first_node', 'start_node']: -                    data[key] = getattr(self, key) -                data['reorder'] = reorder -                pickle.dump(data, f, -1) +            data = {} +            for key in ['lower_bounds', 'upper_bounds', 'node_map', 'node_map_end', 'layers', 'first_node', 'start_node']: +                data[key] = getattr(self, key) +            data['reorder'] = reorder +            np.savez_compressed(cache_path, **data) diff --git a/chroma/sim.py b/chroma/sim.py index f00f536..72fdf3e 100644 --- a/chroma/sim.py +++ b/chroma/sim.py @@ -7,6 +7,7 @@ from chroma import generator  from chroma import gpu  from chroma import event  from chroma import itertoolset +from chroma.tools import profile_if_possible  def pick_seed():      """Returns a seed for a random number generator selected using @@ -14,6 +15,7 @@ def pick_seed():      return int(time.time()) ^ (os.getpid() << 16)  class Simulation(object): +    @profile_if_possible      def __init__(self, detector, seed=None, cuda_device=None,                   geant4_processes=4, nthreads_per_block=64, max_blocks=1024):          self.detector = detector diff --git a/chroma/tools.py b/chroma/tools.py index 1840fae..831a356 100644 --- a/chroma/tools.py +++ b/chroma/tools.py @@ -4,6 +4,12 @@ import datetime  import sys  import math +def filled_array(value, shape, dtype): +    '''Create a numpy array of given `shape` and `dtype` filled with the scalar `value`.''' +    a = np.empty(shape=shape, dtype=dtype) +    a.fill(value) +    return a +  def ufloat_to_str(x):      msd = -int(math.floor(math.log10(x.std_dev())))      return '%.*f +/- %.*f' % (msd, round(x.nominal_value, msd),  | 
