"""
decomp
======
Tools for bulge/disk/halo decomposition
"""
from . import angmom
from .. import array
from .. import filt, util
from .. import config
from . import profile
import numpy as np
import sys
import logging
logger = logging.getLogger('pynbody.analysis.decomp')
[docs]def decomp(h, aligned=False, j_disk_min=0.8, j_disk_max=1.1, E_cut=None, j_circ_from_r=False,
cen=None, vcen=None, log_interp=False, angmom_size="3 kpc"):
"""
Creates an array 'decomp' for star particles in the simulation, with an
integer specifying a kinematic decomposition. The possible values are:
1 -- thin disk
2 -- halo
3 -- bulge
4 -- thick disk
5 -- pseudo bulge
This routine is based on an original IDL procedure by Chris Brook.
**Parameters:**
*h* -- the halo to work on
*j_disk_min* -- the minimum angular momentum as a proportion of
the circular angular momentum which a particle must
have to be part of the 'disk'
*j_disk_max* -- the maximum angular momentum as a proportion of
the circular angular momentum which a particle can
have to be part of the 'disk'
*E_cut* -- the energy boundary between bulge and spheroid. If
None, this is taken to be the median energy of the stars.
*aligned* -- if False, the simulation is recenterd and aligned so
the disk is in the xy plane as required for the rest of
the analysis.
*cen* -- if not None, specifies the center of the halo. Otherwise
it is found. This has no effect if aligned=True
*vcen* -- if not None, specifies the velocity center of the
halo. Otherwise it is found. This has no effect if
aligned=True
*j_circ_from_r* -- if True, the maximum angular momentum is
determined as a function of radius, rather than as a function of
orbital energy. Default False (determine as function of energy).
*angmom_size* -- the size of the gas sphere used to determine the
plane of the disk
"""
import scipy.interpolate as interp
global config
# Center, eliminate proper motion, rotate so that
# gas disk is in X-Y plane
if not aligned:
angmom.faceon(h, cen=cen, vcen=vcen, disk_size=angmom_size)
# Find KE, PE and TE
ke = h['ke']
pe = h['phi']
h['phi'].convert_units(ke.units) # put PE and TE into same unit system
te = ke + pe
h['te'] = te
te_star = h.star['te']
te_max = te_star.max()
# Add an arbitrary offset to the PE to reflect the idea that
# the group is 'fully bound'.
te -= te_max
logger.info("te_max = %.2e" % te_max)
h['te'] -= te_max
logger.info("Making disk rotation curve...")
# Now make a rotation curve for the disk. We'll take everything
# inside a vertical height of eps*3
d = h[filt.Disc('1 Mpc', h['eps'].min() * 3)]
try:
# attempt to load rotation curve off disk
r, v_c = np.loadtxt(h.ancestor.filename + ".rot." +
str(h.properties["halo_id"]), skiprows=1, unpack=True)
pro_d = profile.Profile(d, nbins=100, type='log')
r_me = pro_d["rbins"].in_units("kpc")
r_x = np.concatenate(([0], r, [r.max() * 2]))
v_c = np.concatenate(([v_c[0]], v_c, [v_c[-1]]))
v_c = interp.interp1d(r_x, v_c, bounds_error=False)(r_me)
logger.info(" - found existing rotation curve on disk, using that")
v_c = v_c.view(array.SimArray)
v_c.units = "km s^-1"
v_c.sim = d
v_c.convert_units(h['vel'].units)
pro_d._profiles['v_circ'] = v_c
pro_d.v_circ_loaded = True
except:
pro_d = profile.Profile(d, nbins=100, type='log') # .D()
# Nasty hack follows to force the full halo to be used in calculating the
# gravity (otherwise get incorrect rotation curves)
pro_d._profiles['v_circ'] = profile.v_circ(pro_d, h)
pro_phi = pro_d['phi']
#import pdb; pdb.set_trace()
# offset the potential as for the te array
pro_phi -= te_max
# (will automatically be reflected in E_circ etc)
# calculating v_circ for j_circ and E_circ is slow
if j_circ_from_r:
pro_d.create_particle_array("j_circ", out_sim=h)
pro_d.create_particle_array("E_circ", out_sim=h)
else:
if log_interp:
j_from_E = interp.interp1d(
np.log10(-pro_d['E_circ'].in_units(ke.units))[::-1], np.log10(pro_d['j_circ'])[::-1], bounds_error=False)
h['j_circ'] = 10 ** j_from_E(np.log10(-h['te']))
else:
# j_from_E = interp.interp1d(-pro_d['E_circ'][::-1], (pro_d['j_circ'])[::-1], bounds_error=False)
j_from_E = interp.interp1d(
pro_d['E_circ'].in_units(ke.units), pro_d['j_circ'], bounds_error=False)
h['j_circ'] = j_from_E(h['te'])
# The next line forces everything close-to-unbound into the
# spheroid, as per CB's original script ('get rid of weird
# outputs', it says).
h['j_circ'][np.where(h['te'] > pro_d['E_circ'].max())] = np.inf
# There are only a handful of particles falling into the following
# category:
h['j_circ'][np.where(h['te'] < pro_d['E_circ'].min())] = pro_d[
'j_circ'][0]
h['jz_by_jzcirc'] = h['j'][:, 2] / h['j_circ']
h_star = h.star
if 'decomp' not in h_star:
h_star._create_array('decomp', dtype=int)
disk = np.where(
(h_star['jz_by_jzcirc'] > j_disk_min) * (h_star['jz_by_jzcirc'] < j_disk_max))
h_star['decomp', disk[0]] = 1
# h_star = h_star[np.where(h_star['decomp']!=1)]
# Find disk/spheroid angular momentum cut-off to make spheroid
# rotational velocity exactly zero.
V = h_star['vcxy']
JzJcirc = h_star['jz_by_jzcirc']
te = h_star['te']
logger.info("Finding spheroid/disk angular momentum boundary...")
j_crit = util.bisect(0., 5.0,
lambda c: np.mean(V[np.where(JzJcirc < c)]))
logger.info("j_crit = %.2e" % j_crit)
if j_crit > j_disk_min:
logger.warn(
"!! j_crit exceeds j_disk_min. This is usually a sign that something is going wrong (train-wreck galaxy?)")
logger.warn("!! j_crit will be reset to j_disk_min=%.2e" % j_disk_min)
j_crit = j_disk_min
sphere = np.where(h_star['jz_by_jzcirc'] < j_crit)
if E_cut is None:
E_cut = np.median(h_star['te'])
logger.info("E_cut = %.2e" % E_cut)
halo = np.where((te > E_cut) * (JzJcirc < j_crit))
bulge = np.where((te <= E_cut) * (JzJcirc < j_crit))
pbulge = np.where((te <= E_cut) * (JzJcirc > j_crit)
* ((JzJcirc < j_disk_min) + (JzJcirc > j_disk_max)))
thick = np.where((te > E_cut) * (JzJcirc > j_crit)
* ((JzJcirc < j_disk_min) + (JzJcirc > j_disk_max)))
# h_star['decomp', disk] = 1
h_star['decomp', halo] = 2
h_star['decomp', bulge] = 3
h_star['decomp', thick] = 4
h_star['decomp', pbulge] = 5
# Return profile object for informational purposes
return pro_d