Пример #1
0
def markgal(group, max_temp, min_rho) :
    """Mark all the gas particles in group that have temperatures less
    than or equal to max_temp and densities greater than or equal to
    min_rho. This command can be used to mark those gas particles that
    are likely to be in galaxies."""

    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')
    
    # get opposing boundaries from simulation values
    max_rho = charm.getAttributeRange('gas', 'density')[1]
    min_temp = charm.getAttributeRange('gas', 'temperature')[0]
    
    # play the createGroup shuffle
    charm.createGroup_Family('tmp_group_fam', group, 'gas')
    charm.createGroup_AttributeRange('tmp_group1', 'tmp_group_fam', 'density',
                                     min_rho, max_rho)
    charm.createGroup_AttributeRange('tmp_group2', 'tmp_group1',
                                     'temperature', min_temp, max_temp)
    charm.markParticlesGroup('tmp_group2')
    
    # remove the temporary groups to avoid clutter
    charm.deleteGroup('tmp_group_fam')
    charm.deleteGroup('tmp_group1')
    charm.deleteGroup('tmp_group2')
Пример #2
0
def writemark(group, filename) :
    """Write a file which contains the index values for all
    particles in a group. This file is typically used to
    export and import particle markings.
    
    The file has a header row with ntotal ngas nstar."""

    import charm
    
    # check if simulation loaded
    groups = charm.getGroups()
    if groups == None :
        raise StandardError('Simulation not loaded')
    # check if group exists
    if group not in groups :
        raise StandardError('Group ' + group + ' does not exist')
    
    ngas = charm.getNumParticles(group, 'gas')
    nstar = charm.getNumParticles(group, 'star')
    ntotal = ngas + nstar + charm.getNumParticles(group, 'dark')
    
    # record header row to file
    f = open(filename, 'w')
    f.write('%d %d %d\n' % (ntotal, ngas, nstar))
    f.close()
    
    # record index values
    charm.writeIndexes(group, 'gas', filename)
    charm.writeIndexes(group, 'dark', filename)
    charm.writeIndexes(group, 'star', filename)
Пример #3
0
def markbox(group) :
    """Mark the contents of group. Marked particles are indicated by a "mark"
    attribute being set to non-zero."""

    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')
    
    charm.markParticlesGroup(group)
Пример #4
0
def setsphere(group, *args) :
  """Select particles within radius from coordinate."""
  import charm
  try :
    # parse args
    if len(args) < 3 : raise TypeError('Too few arguments')
    if type(args[0]) == str :
        if args[0] == 'pot' or args[0] == 'potential ':
            center = charm.findAttributeMin(args[1], 'potential')
            radius = args[2]
            if(len(args) == 4) : parent = args[3]
            else : parent = 'All'
        elif args[0] == 'com' :
            if type(args[1]) != str : raise StandardError('third argument should be a group')
            if args[2] == 'all' or args[2] == 'All' :
                center = charm.getCenterOfMass(args[1])
                radius = args[3]
                if(len(args) == 5) : parent = args[4]
                else : parent = 'All'
            elif args[2] == 'gas' :
                charm.createGroup_Family("__tmpgroup", args[1], 'gas')
                center = charm.getCenterOfMass(args[1])
                radius = args[3]
                if(len(args) == 5) : parent = args[4]
                else : parent = 'All'
            elif args[2] == 'dark' :
                charm.createGroup_Family("__tmpgroup", args[1], 'dark')
                center = charm.getCenterOfMass(args[1])
                radius = args[3]
                if(len(args) == 5) : parent = args[4]
                else : parent = 'All'
            elif args[2] == 'star' :
                charm.createGroup_Family("__tmpgroup", args[1], 'star')
                center = charm.getCenterOfMass(args[1])
                radius = args[3]
                if(len(args) == 5) : parent = args[4]
                else : parent = 'All'
            else : raise StandardError('unknown particle type: ' + args[2])
        else : raise StandardError('unknown center type: ' + args[0])
    else :
        center = (args[0], args[1], args[2])
        radius = args[3]
        if(len(args) == 5) : parent = args[4]
        else : parent = 'All'

    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')

    charm.createGroupAttributeSphere(group, parent, 'position', center[0], center[1],
                                     center[2], radius)
  except :
      print traceback.format_exc()
Пример #5
0
def getEpsilonDist(group='gal', nBins=50, epsCut=2., center='pot'):
    """Returns a histogram of dimension 2 x nbins of stellar epsilons (jz/jcirc) and mass-fraction.  epsCut defines the histogram bound in both directions. Center can be either 'pot' or 'com'"""
    numRotBins   = 400 #number of points at which we linearly interpolate 
    radiusVCM    = 0.25 #fraction of group radius to use when calculating COM velocity
    radiusAngMom = 0.25 #fraction of group radius to use when calculating stellar angular momentum
    
    
    a = charm.getTime()
    if ((group in charm.getGroups()==False)) & (group !='gal'):
        print "Group does not exist, please try again."
        return
    if (group == 'gal'): 
        rgal = quesoConfig.rgalFraction*virialgroup.getVirialGroup()
        center=findcenter.findCenter(group2='virialGroup', method=center)
        charm.createGroupAttributeSphere('gal', 'All', 'position', center[0], center[1], center[2], rgal)
    else:
        center=findcenter.findCenter(group2=group, method=center)
        
    # Find maximum radius
    charm.createGroup_Family('tmp',group,'star')
    maxRad = getgroupradius.getGroupRadius('tmp', center)
    massDist = [0]*(numRotBins+1)
    rad = [0]*(numRotBins+1)
    radialStep = maxRad/numRotBins
    #populate mass bins
    for i in range(numRotBins+1): 
        charm.createGroupAttributeSphere('tmp', 'All', 'position', center[0], center[1], center[2], radialStep*i)
        massDist[i]  = charm.getAttributeSum('tmp','star','mass')
        massDist[i] += charm.getAttributeSum('tmp','dark','mass')
        massDist[i] += charm.getAttributeSum('tmp','gas' ,'mass')
        #Note: I tried shells instead of spheres and it made no real difference.
        rad[i] = radialStep*i
        
    vCM = getVelCMStars(center2=center, radius=radiusVCM*maxRad) #COM velocity
    angMomVec = getNormalStarAngMom(center2=center, radius=radiusAngMom*maxRad, comVel=vCM) #Ang Mom Velocity
    #vCM = getVelCMStars(center2=center, radius=10/(a*quesoConfig.kpcunit)) #COM velocity
    #angMomVec = getNormalStarAngMom(center2=center, radius=6/(a*quesoConfig.kpcunit), comVel=vCM) #Ang Mom Velocity
    
    param = (center, vCM, angMomVec, maxRad, numRotBins,massDist, nBins,epsCut)
    charm.createGroupAttributeSphere('tmp', 'All', 'position', center[0], center[1], center[2], maxRad)
    charm.createGroup_Family('tmp',group,'star')
    #calc total stellar mass for mass weighted average
    reduceResult = charm.reduceParticle('tmp', mapEpsilon, reduceEpsilon, param) 
    sMass = charm.getAttributeSum('tmp','star', 'mass')
    epsilonDist = [0]*nBins
    eps  = [0]*nBins
    for i in range(nBins): eps[i] = -epsCut+ 2*epsCut/nBins*(i)
    for i in range(len(reduceResult)):
        try:
            bin = reduceResult[i][0]
            epsilonDist[bin] = reduceResult[i][1]/sMass #return the mass fraction
        except:{}
    return (eps, epsilonDist)
Пример #6
0
def metalProfile(group, nbins=20, center="pot", family="bar"):
    if (group in charm.getGroups()) == False:
        print "Group does not exist, please try again."
        return
    if (family in ["star", "bar", "gas"]) == False:
        print 'Invalid family.  Need "bar","gas", or "star"'
        return
    center = findcenter.findCenter(group2=group, method=center)
    radialStep = getgroupradius.getGroupRadius(group, center) / nbins
    metalRad = [0] * (nbins)
    metalProf = [0] * (nbins)

    numStar = charm.getNumParticles(group, "star")
    numGas = charm.getNumParticles(group, "gas")

    if (numGas == 0) & (numStar == 0):
        return metalProf
    if (numStar != 0) & (family == "star"):
        charm.createGroup_Family("groupMetStar", group, "star")
        metalStar = charm.reduceParticle("groupMetStar", massMetal, reduceMassMetal, (radialStep, center))
        for i in range(len(metalStar)):
            metalProf[metalStar[i][0]] = metalStar[i][1] / metalStar[i][2]

    if (numGas != 0) & (family == "gas"):
        charm.createGroup_Family("groupMetGas", group, "gas")
        metalGas = charm.reduceParticle("groupMetGas", massMetal, reduceMassMetal, (radialStep, center))
        for i in range(len(metalGas)):
            metalProf[metalGas[i][0]] = metalGas[i][1] / metalGas[i][2]

    if family == "bar":
        charm.createGroup_Family("groupMetGas", group, "gas")
        charm.createGroup_Family("groupMetStar", group, "star")
        metalProfGas = [[0, 0]] * nbins
        metalProfStar = [[0, 0]] * nbins

        metalGas = charm.reduceParticle("groupMetGas", massMetal, reduceMassMetal, (radialStep, center))
        for i in range(len(metalGas)):
            metalProfGas[metalGas[i][0]][0] = metalGas[i][1]
            metalProfGas[metalGas[i][0]][1] = metalGas[i][2]
        metalStar = charm.reduceParticle("groupMetStar", massMetal, reduceMassMetal, (radialStep, center))
        for i in range(len(metalStar)):
            metalProfStar[metalStar[i][0]][0] = metalStar[i][1]
            metalProfStar[metalStar[i][0]][1] = metalStar[i][2]
        for i in range(len(metalProf)):
            metalTot = metalProfStar[i][0] + metalProfGas[i][0]
            massTot = metalProfStar[i][1] + metalProfGas[i][1]
            if massTot != 0:
                metalProf[i] = metalTot / massTot
    for i in range(nbins):
        metalRad[i] = (i + 0.5) * radialStep
    return (metalRad, metalProf)
Пример #7
0
def setbox(group, xcenter, ycenter, zcenter, xradius, yradius, zradius,  sourcegroup='All') :
    """Select particles within a box centered on coordinate which extends
    radius in either direction along each axis."""
    
    import charm
    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')

    edge = [xradius, yradius, zradius]
    corner = map(lambda center, radius : center - radius, [xcenter, ycenter, zcenter], edge)
    edge = map(lambda radius : radius * 2, edge)

    charm.createGroupAttributeBox(group, sourcegroup, 'position', corner[0], corner[1], corner[2], edge[0], 0, 0, 0, edge[1], 0, 0, 0, edge[2])
Пример #8
0
def calcGalacticCoordinates(angMomGroup=None, center ='pot'):
    '''Assigns particles a position in the galactic coordinate system. galVelocity = (v+Hr).  angMomGroup'''
    #===========================================================================
    # --Overview--
    # 1.Form transformation matrix
    # 2.Translate the coordinates to the center of the galaxy (or COM velocity)
    # 3.Rotate the coordinate system to be aligned with the angular momentum vector
    # 4.Write information to vector attributes
    #===========================================================================
    if ((angMomGroup in charm.getGroups())==False) & (angMomGroup!=None):
        print "Group does not exist, please try again."
        return
    ##############################
    # Actual Routine
    ##############################
    if (angMomGroup==None):
        angMomGroup = 'angMomGroup'
        cnt = findcenter.findCenter(method=center)
        virialRadius = virialgroup.getVirialGroup()
        charm.createGroupAttributeSphere('angMomGroup', 'All', 'position', cnt[0], cnt[1], cnt[2], virialRadius*quesoConfig.rgalFraction)
        charm.createGroup_Family('angMomGroup', 'angMomGroup', 'gas')
        charm.createGroup_AttributeRange('angMomGroup', 'angMomGroup', 'temperature', 0, quesoConfig.coldGasTemp)
    else: 
        cnt = findcenter.findCenter(group2=angMomGroup,method=center)
        
    angmom.getAngMomVector(group=angMomGroup,center=center)
    # Transformation Matrix
    galZ = vectormath.normalizeVector(angmom.getAngMomVector(group=angMomGroup,center=center))
    galX = vectormath.normalizeVector(vectormath.crossVectors((0,1,0),galZ))
    galY = vectormath.normalizeVector(vectormath.crossVectors(galZ,galX))
    transMatrix = (galX,
                   galY,
                   galZ)
    
    fHubble       = quesoConfig.fHubble0*math.sqrt(quesoConfig.omega0/charm.getTime()**3+(1-quesoConfig.omega0))
    # Galactic Position                    
    charm.createVectorAttribute('star','galPosition')
    charm.createVectorAttribute('gas', 'galPosition')
    charm.createVectorAttribute('dark','galPosition')
    param = (cnt, transMatrix)
    charm.runLocalParticleCodeGroup('All', calcGalPosition, param)
    # Galactic velocity
    vCM=angmom.getVelCM(angMomGroup)
    param = (cnt, transMatrix,vCM, fHubble)
    charm.createVectorAttribute('star','galVelocity')
    charm.createVectorAttribute('gas', 'galVelocity')
    charm.createVectorAttribute('dark','galVelocity')
    charm.runLocalParticleCodeGroup('All', calcGalVelocity, param)
    return
Пример #9
0
def markgal(group, maxTemp, minRho) :
    """Mark gas particles that are likely to be in galaxies based on selected
    temperature and density thresholds. Marked particles are stored in a group
    called "mark" which will be replaced every time a marking command is run."""
    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')
    
    # take only gas particles
    charm.createGroup_Family('mark', group, 'gas')
    # get min temp, get max density
    minTemp = charm.getAttributeRangeGroup('mark', 'gas', 'temperature')[0]
    maxRho = charm.getAttributeRangeGroup('mark', 'gas', 'density')[1]
    # reduce members according to criteria
    charm.createGroup_AttributeRange('mark', 'mark', 'temperature', minTemp, maxTemp)
    charm.createGroup_AttributeRange('mark', 'mark', 'density', minRho, maxRho)
Пример #10
0
def getMomentInertiaTensor(group2,center='pot'):
    '''Return the moment of inertia tensor for given group.  center can be 'pot' or 'com'.  This is in physical units.
        [[Ixx, Ixy, Ixz],
         [Iyx, Iyy, Iyz],
         [Izx, Izy, Izz]]
    '''
    group = group2
    if ((group2 in charm.getGroups())==False):
        print "Group does not exist, please try again."
        return
    center = findcenter.findCenter(group2=group,method=center)
    triangleTensor = charm.reduceParticle(group , mapMomentInertiaTensor, reduceMomentInertiaTensor, center)[0][1]
    fullTensor = [[0]*3,[0]*3,[0]*3]
    units = quesoConfig.msolunit*((charm.getTime()*quesoConfig.kpcunit)**2)
    fullTensor[0] = vectormath.multVectorScalar(units, triangleTensor[0][0:3])
    fullTensor[1] = vectormath.multVectorScalar(units, [triangleTensor[0][1],triangleTensor[1][0],triangleTensor[1][1]])
    fullTensor[2] = vectormath.multVectorScalar(units, [triangleTensor[0][2],triangleTensor[1][1],triangleTensor[2][0]])
    return fullTensor
Пример #11
0
def getMeanMetals(group , family='bar'):
    if ((group in charm.getGroups())==False):
        print "Group does not exist, please try again."
        return
    metalStar = [(0,0,0)]
    metalGas  = [(0,0,0)]        
    numStar = charm.getNumParticles(group, 'star')
    numGas  = charm.getNumParticles(group, 'gas')
    
    if (numStar !=0) & (family=='bar' or family=='star'):
        charm.createGroup_Family('groupMetStar',group, 'star')
        metalStar = charm.reduceParticle('groupMetStar',massMetal,reduceMassMetal,None)
    if (numGas !=0) & (family=='bar' or family=='gas'):
        charm.createGroup_Family('groupMetGas',group , 'gas')
        metalGas  = charm.reduceParticle('groupMetGas' ,massMetal,reduceMassMetal,None)
    if (numGas==0) & (numStar==0):
        return 0
    metals = (metalStar[0][1] + metalGas[0][1])/(metalStar[0][2]+metalGas[0][2])
    return metals
Пример #12
0
def getAngMomVector(group, center='pot'):
    """Returns specific angular momentum vectors in physical units.  Center can be 'pot' or 'com'"""
    if ((group in charm.getGroups())==False):
        print "Group does not exist, please try again."
        return
    def multVectorScalar(scalar,vector):
        return (vector[0]*scalar,vector[1]*scalar,vector[2]*scalar)
    center = findcenter.findCenter(group2=group,method=center)
    #find COM Velocity
    vCM=getVelCM(group)
    
    angMomVector  = charm.reduceParticle(group, getParticleAngMomentum, sumAngMomentum, [center,vCM]) 
    mass   = charm.getAttributeSum(group, 'gas',  'mass')
    mass  += charm.getAttributeSum(group, 'star', 'mass')
    mass  += charm.getAttributeSum(group, 'dark', 'mass')
    unitConvert = quesoConfig.kpcunit*quesoConfig.velocityunit #masses are divided out anyway.
    #Convert this into a specific angular momentum 3-vector
    angMomVector    = multVectorScalar(unitConvert/mass, (angMomVector[0][1] ,angMomVector[0][2] ,angMomVector[0][3]))
    return angMomVector
Пример #13
0
def boxstat(group, family='all') :
    """Print statistics for a group as in the tipsy boxstat command
    Arguments are group and family
    
    Return physical parameters of particles:
        number, mass, center of box, size of box, center of mass, 
        center of mass's velocity, angular momentum vector.

    Format output to say if gas, dark, star, baryon, or all.
    
    Check error cases: Bad input, Not a proper data type, Box not loaded."""
    
    import charm
    # check if simulation loaded
    if charm.getGroups() == None :
        raise StandardError('Simulation not loaded')

    # There is a potential issue with the capitalization of "all" being mixed
    # in different implementations. Take either.
    if family == 'All':
        family = 'all'
    if group == 'all':
        group = 'All'    

    # Prepare values based on selected family
    if family == 'all':
        nPartGas = charm.getNumParticles(group, 'gas')
        nPartDark = charm.getNumParticles(group, 'dark')
        nPartStar = charm.getNumParticles(group, 'star')
        mass = charm.getAttributeSum(group, 'gas', 'mass')
        mass += charm.getAttributeSum(group, 'dark', 'mass')
        mass += charm.getAttributeSum(group, 'star', 'mass')
        bBox = list(charm.getAttributeRangeGroup(group, 'gas', 'position'))
        bBoxTmp = charm.getAttributeRangeGroup(group, 'dark', 'position')
        bBox[0] = map(lambda x, y : min(x,y), bBox[0], bBoxTmp[0])
        bBox[1] = map(lambda x, y : max(x,y), bBox[1], bBoxTmp[1])
        bBoxTmp = charm.getAttributeRangeGroup(group, 'star', 'position')
        bBox[0] = map(lambda x, y : min(x,y), bBox[0], bBoxTmp[0])
        bBox[1] = map(lambda x, y : max(x,y), bBox[1], bBoxTmp[1])
        groupfamname = group
    elif family == 'baryon':
        nPartGas = charm.getNumParticles(group, 'gas')
        nPartStar = charm.getNumParticles(group, 'star')
        mass = charm.getAttributeSum(group, 'gas', 'mass')
        mass += charm.getAttributeSum(group, 'star', 'mass')
        bBox = list(charm.getAttributeRangeGroup(group, 'gas', 'position'))
        bBoxTmp = charm.getAttributeRangeGroup(group, 'star', 'position')
        bBox[0] = map(lambda x, y : min(x,y), bBox[0], bBoxTmp[0])
        bBox[1] = map(lambda x, y : max(x,y), bBox[1], bBoxTmp[1])
        groupfamname = group
    else :
        nPart = charm.getNumParticles(group, family)
        mass = charm.getAttributeSum(group, family, 'mass')
        bBox = charm.getAttributeRangeGroup(group, family, 'position')
        groupfamname = group + 'FAM' + family
        charm.createGroup_Family(groupfamname, group, family)

    # Derive box properties
    size = map(lambda min, max : max - min, bBox[0], bBox[1])
    center = map(lambda min, max : 0.5*(max + min), bBox[1], bBox[0])
    mmoment = charm.reduceParticle(groupfamname, centmassmap, centmassreduce, None)
    cm = [mmoment[0][1]/mass, mmoment[0][2]/mass, mmoment[0][3]/mass]
    vmoment = charm.reduceParticle(groupfamname, centmassVelmap, centmassreduce, None)
    vcm = [vmoment[0][1]/mass, vmoment[0][2]/mass, vmoment[0][3]/mass]
    angmom1 = charm.reduceParticle(groupfamname, angmomMap, centmassreduce, None)

    # Apply parallel axis theorem and make specific
    angmom = [0,0,0]
    angmom[0] = (angmom1[0][1] - mass*(cm[1]*vcm[2] - cm[2]*vcm[1]))/mass
    angmom[1] = (angmom1[0][2] - mass*(cm[2]*vcm[0] - cm[0]*vcm[2]))/mass
    angmom[2] = (angmom1[0][3] - mass*(cm[0]*vcm[1] - cm[1]*vcm[0]))/mass

    # Write output header according to type of particles
    if family == 'all' :
        print 'number of dark, gas and star particles =', nPartDark, nPartGas, nPartStar
    elif family == 'baryon' :
        print 'number of baryon particles =', (nPartGas + nPartStar)
    else:
        print 'number of', family, 'particles =', nPart
    # Write physical parameters
    print 'mass =', mass
    print 'center coordinates =', center
    print 'size =', size
    print 'center of mass coordinates =', cm
    print 'center of mass velocity =', vcm
    print 'angular momentum vector =', angmom
Пример #14
0
def do_meanmwt() :
    if 'AllFAMgas' not in charm.getGroups() :
        charm.createGroup_Family('AllFAMgas', 'All', 'gas')
    if 'meanmwt' not in charm.getAttributes('gas') :
        charm.createScalarAttribute('gas', 'meanmwt')
    charm.runLocalParticleCodeGroup('AllFAMgas', setvals, None)
Пример #15
0
def profile(group='All', center='pot', family='all', projection='sph', bin_type='log', nbins=30, filename='profile.DAT', min_radius=0., fit_radius=0.) :
  """Perform the Tipsy profile() function.
    
    profile is a command that produces a file of  name,  file-
    name,  that  contains various physical profiles as a func-
    tion of radius for particles of type particle in group.

    The  center  of the radial distribution is taken to be the
    center of mass of the particles of the  type  particle  in
    group center. If  the  string  "pot"  is given  instead of
    a  group  name for  the  argument center, the  particle in
    group  with  the  lowest  potential  energy is used as the
    center of the radial bins.

    The projection argument refers to the shape  of  the  bins
    and  the bin_type argument refers to how the particles are
    binned, either linearly or log in radius.  There are nbins
    bins  distributed  equally  in either radius or log radius
    between min_radius and the size of group.

    The  output file has the following columns: radius, number
    in bin, density, cumulative mass (M(<r)), circular  veloc-
    ity (sqrt(M(<r)/r)), radial velocity, radial velocity dis-
    persion, tangential velocity, tangential velocity  disper-
    sion,  specific  angular momentum (j),j_theta, j_phi.  The
    j_theta and j_phi angles specify the direction of the spe-
    cific  angular  momentum vector (in degrees). If family is
    is either gas,  baryon, or all,  four  additional  columns
    containing the mean mass weighted gas density, mass weigh-
    ted gas temperature, mass weighted gas pressure, and  mass
    weighted  gas  entropy  are  added.  If particle is either
    star, baryon, or  all  an additional column containing the
    stellar  luminosity  in the Johnson V band is added.  This
    is  calculated  using  the  ages  of  the  star  particles
    as in Katz (Ap.J. 391: 502, 1992).
    
    Note: some rarer configuration options are not implemented
    e.g. non-uniform UV for star luminosity calculation."""
    
  try:
    import charm
    # check initialization state of meanmwt attribute
    if family in ['all', 'gas', 'baryon'] and not 'meanmwt' in charm.getAttributes('gas') :
        print 'Attribute meanmwt not initialized. Initializing...'
        import load_meanmwt
        load_meanmwt.do_meanmwt()
    print '\n'
    # these should be moved to tipsyf
    # fn to take the dot product of two vectors in R3
    def dotprod(a, b) :
        return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
    # fn to take the cross product of two vectors in R3
    def crossprod(a, b) :
        return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
    # fn to multiply a vector v by a scalar c
    def scalevec(c, v) :
        return [c * v[0], c * v[1], c * v[2]]
    # fn to add two vectors
    def addvec(a, b) :
        return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
    # fn to subtract vector b from vector a
    def subvec(a, b) :
        return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
    # fn to find the length of a vector
    def vlength(a) :
        import math
        return math.sqrt(pow(a[0], 2) + pow(a[1], 2) + pow(a[2], 2))
    # fn to combine two vectors, weighting by their corresponding mass scalars
    def weightvecs(m1, x1, m2, x2) :
        return [(x1[0]*m1 + x2[0]*m2)/(m1+m2), (x1[1]*m1 + x2[1]*m2)/(m1+m2), (x1[2]*m1 + x2[2]*m2)/(m1+m2)]
    
    # scrub input
    # check if simulation loaded, store group list
    groups = charm.getGroups()
    if groups == None :
        raise StandardError('Simulation not loaded')
    # case is inconsistent between groups and families, work for either
    if group == 'all' :
        group = 'All'
    if family == 'All' :
        family = 'all'
    # deal with group or center group does not exist
    if group not in groups :
        raise StandardError('Group does not exist!')
    if (center not in groups) and (center != 'pot') :
        raise StandardError('Center group does not exist!')
    # set up families iterator and throw family related exceptions
    families = charm.getFamilies()
    if families == None :
        raise StandardError('Simulation loaded but no families present.')
    isbaryon = 0
    if family == 'baryon' :
        isbaryon = 1
        if 'dark' in families:
            families.remove('dark')
        if len(families) == 0 :
            raise StandardError('No baryon particles present in simulation.')
    if (family != 'all') and (family != 'baryon') :
        if family not in families :
            raise StandardError('Family not present in simulation.')
        else :
            families = [family]
    if len(families) == 0 :
        raise StandardError('List of families to process has zero length.')
    # create groups containing one family each, store names in famgroups list
    famgroups = []
    for each in families :
        famgroup = group + 'FAM' + each
        charm.createGroup_Family(famgroup, group, each)
	if charm.getNumParticles(famgroup, each) != 0 :
            famgroups += [famgroup]
    if len(famgroups) == 0 :
        raise StandardError('No particles in group of selected type.')
    # validate other inputs
    if bin_type == 'linear' :
        bin_type = 'lin'
    elif bin_type == 'logarithmic' :
        bin_type = 'log'
    elif bin_type not in ['lin', 'log'] :
        raise StandardError('Value of bin_type must be lin or log.')
    if (int(nbins) != nbins) or (nbins < 1) :
        raise StandardError('Value of nbins must be a positive integer.')
    if not min_radius >= 0 :
        raise StandardError('Value of min_radius cannot be negative.')
    if min_radius == 0 and bin_type == 'log' :
        min_radius = config.LOG_MIN
        print 'Parameter min_radius set to ' + str(min_radius) + ' to accomodate logarithmic binning.'
    if not fit_radius >= 0 :
        raise StandardError('Value of fit_radius cannot be negative.')

    # begin calculating parameters

    # find center point
    if center == 'pot' :
        center = charm.findAttributeMin(group, 'potential')
    else:
        center = charm.getCenterOfMass(center)
    
    # get center_vel and center_angular_mom
    # families must be parsed individually then combined
    vmdata = charm.reduceParticle(famgroups[0], vmmap, vmreduce, None)
    if vmdata == None :
        raise StandardError('MapReduce returned NoneType object.')
    center_vel = vmdata[0][2]
    center_angular_mom = vmdata[0][3]
    # if there are additional families, parse each then combine results weighting by mass
    if len(famgroups) > 1 :
        old_mass = vmdata[0][1]
        for i in range(len(famgroups)-1) :
            vmdata = charm.reduceParticle(famgroups[i+1], vmmap, vmreduce, None)
            new_mass = vmdata[0][1]
            center_vel = weightvecs(old_mass, center_vel, new_mass, vmdata[0][2])
            center_angular_mom = weightvecs(old_mass, center_angular_mom, new_mass, vmdata[0][3])
            old_mass += new_mass
    
    # for an elliptical projection, use Katz to get shape & then find vel, mom
    # when restoring this, find out what happened to center_ell?
    if projection == 'ell' :
        if fit_radius == 0. :
            print 'fit_radius = 0 in an elliptical projection.\nusing all particles in batch to find shape of ellipses.'
        ell_matrix = [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]
        center_ell = [0., 0., 0.]
        ba = ca = phi = theta = psi = 0.
        center = list(center)
        shape_data = elliptical.find_shape(famgroups, center, fit_radius, ell_matrix, center_ell)
        center = tuple(center)
        if type(shape_data) == type(None) :
            print 'find_shape() did not succeed'
            return
            # raise StandardError('Problem in elliptical.find_shape(): no fit found.')
        else :
            ba, ca, phi, theta, psi = shape_data
            print 'b/a = %g, c/a = %g' % (ba, ca)
        center = [0.]*config.MAXDIM
        center_vel = [0.]*config.MAXDIM
        center_angular_mom = [0.]*config.MAXDIM
        vel_data = elliptical.find_vel(famgroups,center,center_vel,center_angular_mom, ell_matrix, center_ell, ba, ca, fit_radius)
    else :
        ba, ca, phi, theta, psi, center_ell, ell_matrix = (0., 0., 0., 0., 0., [0.]*config.MAXDIM, [[0.]*config.MAXDIM]*config.MAXDIM)

    # find max_radius
    # maxradmap iterates over all particles and compares each radius to the previous max found
    p_param = (center)
    max_radius = 0.
    if family == 'all' :
        max_radius = charm.reduceParticle(group, maxradmap, maxradreduce, p_param)[0][1]
    else :
        for i in range(len(famgroups)) :
            fam_radius = charm.reduceParticle(famgroups[i], maxradmap, maxradreduce, p_param)[0][1]
            max_radius = max(max_radius, fam_radius)
    
    # TIPSY uses an approximation for max_radius which can't be adequately emulated here.
    # for the run99 data file, the value of max_radius is 8.18829; set it manually.
    # max_radius = 8.18829
    
    # determine bin_size
    # if min_radius > 0, then bin 0 should have the specified radius.
    # other than this exception, all bins have constant size in either lin or log space as specified
    if min_radius > max_radius and nbins > 1 :
        raise StandardException('Value for min_radius encompasses all particles; cannot bin data.')
    # min_radius > 0 case creates a scenario where nbins = 1 would create a /0 error, must handle here.
    if nbins == 1 :
        bin_size = max_radius
        # bin_type and min_radius are superfluous to nbins = 1 case
        bin_type = 'lin'
        min_radius = 0.
    # find size in lin space
    elif bin_type == 'lin' :
        if min_radius > 0. :
            bin_size = (max_radius - min_radius) / float(nbins - 1)
        else :
            bin_size = (max_radius - min_radius) / float(nbins)
    # find size in log space
    else :
        min_radius = math.log10(min_radius)
        bin_size = (math.log10(max_radius) - min_radius) / float(nbins - 1)
    
    # calculate bin boundary radii
    bounds = [0.] * (nbins + 1)
    # the first boundary radius is affected by min_radius, handle
    if bin_type == 'lin' and min_radius == 0.:
        bounds[1] = bin_size
    else :
        bounds[1] = min_radius
    # each following bound should be bound[previous] + bin_size
    for i in range(2, nbins + 1) :
        bounds[i] = bounds[i - 1] + bin_size
    # if logarithmic binning, convert values into linear space
    if bin_type == 'log' :
        for i in range(1, nbins + 1) :
            bounds[i] = pow(10., bounds[i])

    starlf = star_lum.star_lum()
    
    sim_time = charm.getTime()
    
    # do calculations which must see individual particles.
    # the set of attributes on a particle varies by family, so each family must be processed seperately
    params = [None, isbaryon, bounds, projection, nbins, bin_type, bin_size, min_radius, max_radius, center, center_vel, ell_matrix, center_ell, ba, ca, config.msolunit, config.gasconst, sim_time, config.time_unit, starlf.age, starlf.lum, starlf.lumv_fit, starlf.vv_dat, starlf.vv_fit, starlf.bv_dat, starlf.bv_fit, center_angular_mom]
    fam_data = [None] * len(famgroups)
    for i in range(len(famgroups)) :
        params[0] = families[i]
        fam_data[i] = charm.reduceParticle(famgroups[i], basemap, basereduce, tuple(params))
        
    # shows per-family result of MapReduce, sometimes useful for debugging
    #    if debug_flag == 1 :
    #        print '\nThe current family is: ' + families[i]
    #        for line in fam_data[i] :
    #            print line
    if len(fam_data) < 1 or fam_data == None :
        raise StandardException('MapReduce for fam_data has length zero or is NoneType.')
    
    # sum results into one list.
    # Note that if a bin has no particles, it will be missing from the reduce result. data[] should have no missing rows.
    # values are: bin, number, mass, vel_radial, vel_radial_sigma, vel_tang_sigma, angular_mom[x,y,z], lum, density, temp, pressure, entropy, gas_mass
    data = [None] * nbins
    for i in range(nbins) :
        data[i] = [i, 0, 0., 0., 0., 0., [0., 0., 0.], 0., 0., 0., 0., 0., 0.]    
    for fam in fam_data :
        for row in fam :
            bin = row[0]
            # merge fields 0 thru 5
            for i in range(1, 6) :
                data[bin][i] += row[i]
            # merge field 6 (angular momentum vector)
            for i in range(3) :
                data[bin][6][i] += row[6][i]
            # merge fields 7 thru 12
            for i in range(7, 13) :
                data[bin][i] += row[i]
    if not len(data) == nbins :
        raise StandardError('nbins != length of result after basemap')

    # calculate remaining "base" values that did not need to be handled in the map
    mass_r = 0.
    for i in range(nbins) :
        # get max radius, min radius, and mean radius
        r_max = bounds[i + 1]
        r_min = bounds[i]
        r_mean = (r_max + r_min) / 2.
        # number of particles in bin
        number = data[i][1]
        # if the bin is empty then some calculations e.g. 1/mass will explode; catch these.
        if number != 0 :
            # find volume by projection
            pi = 3.141592653589793
            if projection == 'ell' :
                volume = 4. * ba * ca / 3. * pi * (pow((r_max),3) - pow((r_min),3))
            elif projection == 'cyl' :
                volume = pi * (pow((r_max),2) - pow((r_min),2))
            else :
                volume = (4. / 3.) * pi * (pow(r_max,3) - pow(r_min,3))
            # unpack values. deal with non-sum calculations, largely n / mass
            mass = data[i][2]
            mass_r += mass
            vel_radial = data[i][3] / mass
            vel_radial_sigma = data[i][4] / mass
            if vel_radial_sigma > vel_radial**2 :
                vel_radial_sigma = math.sqrt(vel_radial_sigma - vel_radial**2)
            else :
                vel_radial_sigma = 0.
            vel_tang_sigma = data[i][5] / mass
            ang_mom = scalevec(1./mass, data[i][6])
            ang = vlength(ang_mom)
            if ang > 0.0 :
                ang_theta = 180.0 * math.acos(ang_mom[2] / ang) / pi
            else :
                ang_theta = 0.0
            ang_phi = 180.0 * math.atan2(ang_mom[1], ang_mom[0]) / pi
            rho = mass / volume
            vel_circ = ang / r_mean
            c_vel = math.sqrt(mass_r / r_max)
            if vel_tang_sigma > vel_circ**2 :
                vel_tang_sigma = math.sqrt(vel_tang_sigma - vel_circ**2)
            else :
                vel_tang_sigma = 0.
            # star stuff
            lum_den = data[i][7] / volume
            # gas stuff
            if 'gas' in families and data[i][12] > 0 :
                density  = data[i][8]  / data[i][12]
                temp     = data[i][9]  / data[i][12]
                pressure = data[i][10] / data[i][12]
                entropy  = data[i][11] / data[i][12]
            else :
                density  = 0.
                temp     = 0.
                pressure = 0.
                entropy  = -config.HUGE
            data[i] = [r_max, number, rho, mass_r, c_vel, vel_radial, vel_radial_sigma, vel_circ, vel_tang_sigma, ang, ang_theta, ang_phi, density, temp, pressure, entropy, lum_den]
        else :
            c_vel = math.sqrt(mass_r / r_max)
            data[i] = [r_max, 0, 0., mass_r, c_vel, 0., 0., 0., 0., 0., 0.,
                       0., 0., 0., 0., 0., 0.]
    
    # build header row
    # may need to update the exact text used for clarity or for consistency with TIPSY
    headers = ('radius', 'number', 'rho', 'mass_r', 'c_vel', 'vel_radial', 'vel_radial_sigma', 'vel_circ', 'vel_tang_sigma', 'ang', 'ang_theta', 'ang_phi')
    if family in ['gas', 'baryon', 'all'] :
        headers += ('mmwg_density', 'mwg_temp', 'mwg_pres', 'mwg_entr')
    if family in ['star', 'baryon', 'all'] :
        headers += ('lum_V',)
    
    # write output to file
    f = open(filename, 'w')
    # write headers according to families present
    f.write('#')
    for i in range(len(headers)) :
        f.write(' ' + headers[i])
    f.write('\n')
    # write data according to families present
    for i in range(nbins) :
        f.write('%g %d %g %g %g %g %g %g %g %g %g %g' % tuple(data[i][0:12]))
        if 'gas' in families :
            f.write(' %g %g %g %g' % tuple(data[i][12:16]))
        if 'star' in families :
            f.write(' %g' % tuple(data[i][16:17]))
        f.write('\n')
    f.close()

    # there is no current mechanism to delete groups.
    # if one is added, delete temporary working groups here.
  except :
    print traceback.format_exc()