"""
Galactic bulge/disk/halo decomposition
"""
import logging
import numpy as np
from .. import array, config, filt, transformation, util
from . import angmom, profile
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,
log_interp=False, angmom_size="3 kpc"):
"""Creates an array 'decomp' for star particles in the simulation, with an integer specifying components.
The possible values of the components 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.
Arguments
---------
h : SimSnap
The simulation snapshot to analyze
aligned : bool
If True, the simulation is assumed to be already aligned so that the disk is in the xy plane.
Otherwise, the simulation is recentered and aligned into the xy plane.
j_disk_min : float
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 : float
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 : float
The energy boundary between bulge and spheroid. If None, this is taken to be the median energy of the stars.
j_circ_from_r : bool
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 : str
The size of the disk to use for calculating the angular momentum vector. Default is "3 kpc".
"""
import scipy.interpolate as interp
global config
# Center, eliminate proper motion, rotate so that
# gas disk is in X-Y plane
if aligned:
tx = transformation.NullTransformation(h)
else:
tx = angmom.faceon(h, disk_size=angmom_size)
with tx:
# 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_number"]), 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 Exception:
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.warning(
"!! j_crit exceeds j_disk_min. This is usually a sign that something is going wrong (train-wreck galaxy?)")
logger.warning("!! 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', 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