Source code for pynbody.halo

"""

halo
====

Implements halo catalogue functions. If you have a supported halo
catalogue on disk or a halo finder installed and correctly configured,
you can access a halo catalogue through f.halos() where f is a
SimSnap.

See the `halo tutorial
<http://pynbody.github.io/pynbody/tutorials/halos.html>`_ for some
examples.

"""

import numpy as np
import weakref
import copy
import logging

from .. import snapshot, util

logger = logging.getLogger("pynbody.halo")

class DummyHalo(object):

    def __init__(self):
        self.properties = {}


[docs]class Halo(snapshot.IndexedSubSnap): """ Generic class representing a halo. """ def __init__(self, halo_id, halo_catalogue, *args, **kwa): super(Halo, self).__init__(*args, **kwa) self._halo_catalogue = halo_catalogue self._halo_id = halo_id self._descriptor = "halo_" + str(halo_id) self.properties = copy.copy(self.properties) self.properties['halo_id'] = halo_id if halo_id in halo_catalogue._halos: for key in list(halo_catalogue._halos[halo_id].properties.keys()): self.properties[key] = halo_catalogue._halos[halo_id].properties[key]
[docs] def is_subhalo(self, otherhalo): """ Convenience function that calls the corresponding function in a halo catalogue. """ return self._halo_catalogue.is_subhalo(self._halo_id, otherhalo._halo_id)
# ----------------------------# # General HaloCatalogue class # #-----------------------------#
[docs]class HaloCatalogue(object): """ Generic halo catalogue object. """ def __init__(self, sim): self._base = weakref.ref(sim) self._halos = {} self.lazy_off = util.ExecutionControl() def calc_item(self, i): if i in self._halos: # and self._halos[i]() is not None : if isinstance(self._halos[i],DummyHalo): try: return self._get_halo(i) except: return self._halos[i] else: return self._halos[i] else: h = self._get_halo(i) self._halos[i] = h # weakref.ref(h) return h def __len__(self): return len(self._halos) def __iter__(self): return self._halo_generator() def __getitem__(self, item): if isinstance(item, slice): for x in self._halo_generator(item.start,item.stop) : pass indices = item.indices(len(self._halos)) res = [self.calc_item(i) for i in range(*indices)] return res else: return self.calc_item(item) @property def base(self): return self._base() def _halo_generator(self, i_start=None, i_stop=None) : if len(self) == 0 : return if i_start is None : try : self[0] i = 0 except KeyError : i = 1 else : i = i_start if i_stop is None : i_stop = len(self) while True: try: yield self[i] i+=1 if i!=i_stop and len(self[i]) == 0: continue except RuntimeError: break if i == i_stop: return def _init_iord_to_fpos(self): if not hasattr(self, "_iord_to_fpos"): self._iord_to_fpos = np.empty(self.base['iord'].max()+1,dtype=np.int64) self._iord_to_fpos[self.base['iord']] = np.arange(len(self.base))
[docs] def is_subhalo(self, childid, parentid): """Checks whether the specified 'childid' halo is a subhalo of 'parentid' halo. """ if (childid in self._halos[parentid].properties['children']): return True else: return False
def contains(self, haloid): if (haloid in self._halos): return True else: return False def __contains__(self, haloid): return self.contains(haloid)
[docs] def get_group_array(self): """Return an array with an integer for each particle in the simulation indicating which halo that particle is associated with. If there are multiple levels (i.e. subhalos), the number returned corresponds to the lowest level, i.e. the smallest subhalo.""" raise NotImplementedError
@staticmethod def _can_load(self): return False @staticmethod def _can_run(self): return False
[docs]class GrpCatalogue(HaloCatalogue): """ A generic catalogue using a .grp file to specify which particles belong to which group. """ def __init__(self, sim, array='grp', ignore=None, **kwargs): """Construct a GrpCatalogue, extracting halos based on a simulation-wide integer array with their IDs *sim* - the SimSnap for which the halos will be constructed *array* - the name of the array which should be present, loadable or derivable across the simulation *ignore* - a special value indicating "no halo", or None if no such special value is defined """ sim[array] # trigger lazy-loading and/or kick up a fuss if unavailable self._halos = {} self._array = array self._sorted = None self._ignore = ignore HaloCatalogue.__init__(self,sim) def __len__(self): if self._ignore is None: N = self.base[self._array].max() else: N = self.base[self._array] N = N[N!=self._ignore] N = N.max() if N<0: N=0 return N
[docs] def precalculate(self): """Speed up future operations by precalculating the indices for all halos in one operation. This is slow compared to getting a single halo, however.""" self._sorted = np.argsort( self.base[self._array], kind='mergesort') # mergesort for stability self._boundaries = util.find_boundaries( self.base[self._array][self._sorted])
[docs] def get_group_array(self, family=None): if family is not None: return self.base[family][self._array] else: return self.base[self._array]
def _get_halo_indices(self, i): if self.base is None: raise RuntimeError("Parent SimSnap has been deleted") no_exist = ValueError("Halo %s does not exist" % (str(i))) if self._sorted is None: # one-off selection index = np.where(self.base[self._array] == i) return index else: # pre-calculated if i >= len(self._boundaries) or i < 0: raise no_exist if self._boundaries[i] < 0: raise no_exist start = self._boundaries[i] if start is None: raise no_exist end = None j = i + 1 while j < len(self._boundaries) and end is None: end = self._boundaries[j] j += 1 return self._sorted[start:end] def _get_halo(self, i): x = Halo(i, self, self.base, self._get_halo_indices(i)) if len(x) == 0: raise ValueError("Halo %s does not exist" % (str(i))) x._descriptor = "halo_" + str(i) return x
[docs] def load_copy(self, i): """Load the a fresh SimSnap with only the particle in halo i""" from .. import load return load(self.base.filename, take=self._get_halo_indices(i))
@property def base(self): return self._base() @staticmethod def _can_load(sim, arr_name='grp'): if (arr_name in sim.loadable_keys()) or (arr_name in list(sim.keys())) : return True else: return False
[docs]class AmigaGrpCatalogue(GrpCatalogue): def __init__(self, sim, arr_name='amiga.grp',**kwargs): GrpCatalogue.__init__(self, sim, arr_name) @staticmethod def _can_load(sim,arr_name='amiga.grp'): return GrpCatalogue._can_load(sim, arr_name)
from pynbody.halo.ahf import AHFCatalogue from pynbody.halo.hop import HOPCatalogue from pynbody.halo.adaptahop import AdaptaHOPCatalogue from pynbody.halo.legacy import RockstarIntermediateCatalogue from pynbody.halo.rockstar import RockstarCatalogue from pynbody.halo.subfind import SubfindCatalogue from pynbody.halo.subfindhdf import SubFindHDFSubhaloCatalogue, SubFindHDFHaloCatalogue def _get_halo_classes(): # AmigaGrpCatalogue MUST be scanned first, because if it exists we probably # want to use it, but an AHFCatalogue will probably be on-disk too. _halo_classes = [GrpCatalogue, AmigaGrpCatalogue, AHFCatalogue, RockstarCatalogue, SubfindCatalogue, SubFindHDFHaloCatalogue, RockstarIntermediateCatalogue, HOPCatalogue, AdaptaHOPCatalogue] return _halo_classes