# coding=utf-8
import numpy as np
from scipy import interpolate
try:
import bottleneck as bn
except ImportError:
import numpy as bn
from .hyperspectral_cube import HyperspectralCube as HyperCube
from .galaxy_parameters import GalaxyParameters
from .model_utilities import modelPlots, ModelExt
# CONSTANTS
G = 6.67384e-11 # Gravitational Constant (m^3.kg^-1.s^-2)
SOL_MASS = 2e30 # Solar Mass (kg)
PARSEC = 3e16 # Parsec (m)
# LOGGING CONFIGURATION
import logging
logging.basicConfig(level=logging.INFO)
[docs]class DiskModel(modelPlots, ModelExt):
"""
The first and default galpak `Model` (when unspecified).
It simulates a simple disk galaxy using `DiskParameters`.
flux_profile: 'exponential' | 'gaussian' | 'de_vaucouleurs' | 'sersicN2' | 'user'
The flux profile of the observed galaxy. The default is 'exponential'.
See http://en.wikipedia.org/wiki/Sersic%27s_law
If 'user' provided flux map, specify flux_image. Currently unsupported
thickness_profile: 'gaussian' [default] | 'exponential' | 'sech2' | 'none'
The perpendicular flux profile of the disk galaxy.
The default is 'gaussian'.
If none, the galaxy will be a cylinder
rotation_curve: 'arctan' | 'mass' | 'exp' | 'tanh' | 'isothermal'
The profile of the velocity v(r) can be in
- 'arctan' (default) : ~ Vmax arctan(r/rV), rV=turnover radius
- 'tanh' : Vmax tanh(r/rV), rV=turnover radius
- 'exp' : inverted exponential, 1-exp(-r/rV)
- 'mass' : a constant light-to-mass ratio v(r)=sqrt(G m(<r) / r)
- 'isothermal' : an isothermal rotation curve v(r)=Vmax * sqrt(1-rV/r * arctan(r/rV)
dispersion_profile: 'thick' [default] | 'thin'
The local disk dispersion from the rotation curve and disk thickness
from Binney & Tremaine 2008, see Genzel et al. 2008.
GalPak has 3 components for the dispersion:
- a component from the rotation curve arising from mixing velocities
of a disk with non-zero thickness.
- a component from the local disk dispersion specified by
`disk_dispersion`
- a spatially constant dispersion,
which is the output parameter `velocity_dispersion`.
line: None[default] | dict to fit doublets, use a dictionary with
line['wave']=[l, lref] eg. [3726.2, 3728.9] # Observed or Rest
line['ratio']=[0.8, 1.0] eg. [0.8, 1.0] # The primary line for redshifts is the reddest
redshift: float
The redshift of the observed galaxy, used in mass calculus.
Will override the redshift provided at init ; this is a convenience parameter.
If this is not set anywhere, GalPak will not try to compute the mass.
"""
# fixme: use integers instead of strings here and enjoy the performance gain
FLUX_VALID = ['gaussian', 'exponential', 'de_vaucouleurs', 'sersicN2', 'user']
THICKNESS_VALID = [ 'gaussian', 'exponential', 'sech2', 'none']
DISPERSION_VALID = [ 'thick', 'thin']
CURVE_VALID = ['arctan', 'mass', 'exponential', 'tanh', 'isothermal' ]
def __init__(self,
flux_profile='exponential',
thickness_profile='gaussian',
dispersion_profile='thick',
rotation_curve= 'arctan',
flux_image=None,
line=None,
redshift=None,
cosmology='planck15'):
self.flux_profile = flux_profile
self.thickness_profile = thickness_profile
self.dispersion_profile = dispersion_profile
self.rotation_curve = rotation_curve
self.line = line
self.redshift = redshift
self.cosmology= cosmology
self.logger = logging.getLogger('GalPaK: DiskModel:')
#if using input image
if self.flux_profile == 'user':
self.flux_map_user = flux_image
if self.redshift is not None and self.cosmology is not None:
self.set_cosmology()
self.Parameters = GalaxyParameters #just a copy of Parameter class
#@fixme to be removed
self.hz_profile = thickness_profile #for backwards compatibility
self.disk_dispersion = dispersion_profile #for backwards compatibility
self.GalaxyParameters = GalaxyParameters #for backwards compatibility
#From v2.0beta
def set_cosmology(self):
try:
from colossus.cosmology import cosmology as cosmo
from colossus import halo
from colossus.halo import mass_defs, mass_adv, mass_so, concentration
self.logger.info("setting cosmology with colossus package")
cosmo=cosmo.setCosmology(self.cosmology)
#put h in
h = cosmo.h
self.Ez = cosmo.Ez(self.redshift)
DA = cosmo.angularDiameterDistance(self.redshift) / h ####!!
self.DeltaVir = halo.mass_so.deltaVir(self.redshift)
self.halo = halo #Diemer Halo module for NFW halo models
except ImportError:
try:
from astLib import astCalc
#using astLib for cosmology
self.logger.info("Using AstLib for a 737 cosmology, h,OM, OL= (0.7, 0.3, 0.7)")
h = astCalc.H0 / 100.
self.Ez = astCalc.Ez(self.redshift)
DA = astCalc.da(self.redshift)#for h=0.7
self.DeltaVir = astCalc.DeltaVz(self.redshift)
self.halo = None
self.logger.warning('Best to use colossus package from Diemer& Kratsov for this module')
except ImportError:
#using astropy
from astropy import cosmology
self.logger.info("Using astropy.cosmology WMAP9 only for cosmology")
cosmo = cosmology.default_cosmology.get()
h = cosmo.h
DA = cosmo.angular_diameter_distance(self.redshift)
self.Ez = cosmo.H(self.redshift).value/cosmo.H0
self.DeltaVir = None### not available
self.halo = None
self.logger.warning('Best to use colossus package from Diemer& Kratsov for this module')
self.kpc = DA * 1e3 * np.radians(1./3600)# in unit kpc
#self.hkpc = self.kpc * h
def initial_parameters(self, runner):
"""
Returns an instance of a class extending `ModelParameters`.
You may omit parameters, they will be automatically set to the mean of
the max and min.
"""
# Default initial parameters
return GalaxyParameters(
radius=3.0,
turnover_radius=1.0,
inclination=30,
pa=np.random.uniform(0,360),
maximum_velocity=100.0,
velocity_dispersion=5.0,
flux=runner.flux_est
)
def min_boundaries(self, runner):
"""
Returns an instance of a class extending `ModelParameters`.
You MUST provide all the parameters.
"""
# Default boundaries
return GalaxyParameters(
x=runner.cube.shape[2] / 3.,
y=runner.cube.shape[1] / 3.,
z=runner.cube.shape[0] / 3.,
flux=0.,
radius=0.199,
inclination=-0.5,
pa=0,
turnover_radius=0.01,
maximum_velocity=0.,
velocity_dispersion=0.
)
def max_boundaries(self, runner):
"""
Returns an instance of a class extending `ModelParameters`.
You MUST provide all the parameters.
"""
# changed max radius to 3/8th of cube size.
return GalaxyParameters(
x=runner.cube.shape[2] * 2 / 3.,
y=runner.cube.shape[1] * 2 / 3.,
z=runner.cube.shape[0] * 2 / 3.,
flux=3. * runner.flux_est,
radius= 0.5 * (runner.cube.shape[1]+runner.cube.shape[2]) * 3 / 8.,
inclination=90.5,
pa=360,
turnover_radius=0.5 * (runner.cube.shape[1]+runner.cube.shape[2]) * 3 / 8. / 2.,
maximum_velocity=350.,
velocity_dispersion=180.
)
def sanitize_parameters(self, parameters):
"""
Mutates the parameters with custom logic.
This is called right after parameter jumping in the loop.
"""
inc = parameters['inclination']
if inc<0:
parameters.pa +=180
parameters.pa = parameters.pa % 360. #to deal with circularity
def sanitize_chain(self, chain):
"""
Mutates the `chain` with custom logic.
This is called at the end of the loop, before storing the chain.
"""
pa = chain['pa']
pa_corr = np.where(pa > 180, pa - 360, pa)
chain['pa'] = pa_corr
def setup_random_amplitude(self, amplitude):
"""
Mutates the random `amplitude` of the parameter jump with custom logic.
This is called during the setup of the MCMC runner.
The amplitude is already filled with :
sqrt((min_boundaries - max_boundaries) ** 2 / 12.) * p / v
where
p = number of parameters in the model
v = number of voxels in the cube
"""
amplitude['flux'] *= 0.5 # flux since we start at ~flux since flux is an integrated quantity
amplitude['pa'] *= 1.5
amplitude['maximum_velocity'] *= 1.5 #
amplitude['velocity_dispersion'] *= 1.5 # velocity dispersion
amplitude *= 3 # fixme: document this coefficient
#CUSTOM METHODS
def _set_flux_profile(self, galaxy, radius_cube):
"""
creates a flux_cube in 3D (x,y,z)
returns flux_cube
"""
rad = galaxy['radius']
if self.flux_profile == 'de_vaucouleurs':
flux_cube = self.flux_de_vaucouleurs(radius_cube, rad)
elif self.flux_profile == 'gaussian':
flux_cube = self.flux_gaussian(radius_cube, rad)
elif self.flux_profile == 'exponential':
flux_cube = self.flux_exponential(radius_cube, rad)
elif self.flux_profile == 'sersicN2':
flux_cube = self.flux_sersic(radius_cube, rad, 2.0)
elif self.flux_profile == 'user':
self.logger.error("Not implemented")
#should be face-on view...
flux_cube = self.flux_map_user[None,:,:] #@fixme need thickness?
else:
raise NotImplementedError("Flux profile not supported. Should be '%s' " %
self.model.FLUX_VALID)
return flux_cube
def _set_velocity_profile(self, galaxy, radius_cube):
"""
creates a velocity profile for circular orbits
return v_profile
"""
max_vel = galaxy['maximum_velocity']
rt = galaxy['turnover_radius']
if self.rotation_curve == 'arctan':
v_profile = np.arctan(radius_cube / rt) * max_vel * 2. / np.pi
# For Mass=Light profile
elif self.rotation_curve == 'mass':
v_profile = self._fv_newton(radius_cube, galaxy.radius, max_vel)
# For Feng's inv_exp profile
elif self.rotation_curve == 'exponential':
v_profile = max_vel * (1.0 - np.exp(-radius_cube / rt))
elif self.rotation_curve == 'tanh' :
v_profile = max_vel * np.tanh(radius_cube / rt)
elif self.rotation_curve == 'isothermal' :
v_profile = max_vel * np.sqrt(1 - rt/radius_cube*np.arctan(radius_cube/rt))
else:
raise NotImplementedError("Rotational Curve not supported. Should be '%s' " %
self.model.CURVE_VALID)
return v_profile
# DEFAULT METHODS
def _set_thickness_profile(self, nz, hz, flux_cube):
"""
add the thickness profile to the flux cube
returns flux_cube
"""
if self.thickness_profile == 'gaussian':
flux_cube *= np.exp(-nz**2. / 2. / hz**2.)
elif self.thickness_profile == 'exponential':
flux_cube *= np.exp(-np.sqrt(nz**2.) / hz) # exponential z-profile
elif self.thickness_profile == 'sech2':
flux_cube *= np.cosh(-nz / hz)**(-2.) # sech^2 z-profile
elif self.thickness_profile == 'none':
self.logger.warning("Using no disk thickness profile: "
"this galaxy will become a cylinder!")
flux_cube = flux_cube
else:
raise NotImplementedError("Context profile not supported. Should be '%s'" %
self.model.THICKNESS_VALID)
return flux_cube
def _set_dispersion_profile(self, hz, vtot, radius_cube):
"""
set disk dispersion profile
"""
### ISOTROPIC but this IS the 1-d dispersion in z-direction
# A) using R/h=0.5 (V/sigma)^2 for very thin disk
if self.dispersion_profile == 'thin':
# dimension of r [3D-spatial]
sig_ale_disk = np.sqrt(hz * vtot ** 2. / radius_cube)
# B) using R/h=V/Sigma for compact disk
elif self.dispersion_profile == 'thick':
# dimension of r [3D-spatial]
sig_ale_disk = hz * vtot / radius_cube
# C) Using Binney Merrifield
# elif self.dispersion_profile == 'infinitely_thin':
# dimension of r [3D-spatial]
# sig_ale_disk = np.sqrt(math.tau * Surf_density * G * hz_kpc)
else:
raise ValueError("Disk dispersion is not valid. Should be '%s' " %
self.DISPERSION_VALID)
return sig_ale_disk
###########################################
#HEART of model
###########################################
def _create_maps(self, galaxy, shape):
"""
Creates and returns:
- a cube sized like (`shape`) containing a clean simulation
of a galaxy for the `parameters`.
- the velocity map, which is an image.
- the dispersion (sigma) map, which is an image.
This is called on each loop, to compute the likelihood of each generated
set of parameters. Therefore, it needs to be fast.
parameters: DiskParameters
The parameters upon which the simulated disk galaxy will be built.
shape: Tuple of 3
The 3D shape of the resulting cube.
Eg: (21,21,21)
Returns a tuple.
"""
# Create radial velocities and radial dispersions
flux_cube, vz, vz_map, s_map, sig_map, sigz_disk_map, sig_intr = \
self._compute_galaxy_model(galaxy, shape)
# normalize to real flux
flux_map = flux_cube.sum(0)
flux_map = galaxy.flux * flux_map / flux_map.sum()
return flux_map, vz_map, s_map
def _create_cube(self, shape, f_map, v_map, s_map, z_step_kms, zo=None):
"""
Creates a 3D cube from Flux, Velocity and Dispersion maps.
galaxy: GalaxyParameters
shape: tuple
Defines the created cube's shape
f_map: ndarray
2D image of the flux
v_map: ndarray
2D image of the velocity
s_map: ndarray
2D image of the dispersion
z_step_kms: float
in km/s
zo: int
The centroid (in pixel) in the z-direction
If not set, will be the middle of the z-axis
"""
vmax = z_step_kms * (shape[0] - 1) / 2.
if zo is None:
vzero = vmax
else:
vzero = z_step_kms * zo # fixme: shouldn't this be with zo-0.5 ?
# Array of velocities : array of indices from to 0 to Vmax
velocities = np.linspace(0, 2 * vmax, shape[0]) - vzero
# Create a copy of Vmap in each z plane
tmpcube = np.outer(np.ones_like(velocities), v_map)
vcube = tmpcube.reshape(shape)
# Create a copy of Smap in each z plane
tmpcube = np.outer(np.ones_like(velocities), s_map)
scube = tmpcube.reshape(shape)
# Create a copy of velocities in each location
if z_step_kms > 0:
vbegin = np.min(velocities)
vfinal = np.max(velocities)
else:
vbegin = np.max(velocities)
vfinal = np.min(velocities)
# Create grid of velocities
vgrid, y, x = np.mgrid[
vbegin: vfinal + 0.9 * z_step_kms: z_step_kms,
0: np.shape(v_map)[0],
0: np.shape(v_map)[1]
]
# Apply Gaussian
finalcube = np.exp(-0.5 * (vgrid - vcube) ** 2 / (scube ** 2))
# For doublets, add the blue component
# deltaV is defined as (l2-l1)/l2 * c ie from the reddest line
if self.line is not None:
# Delta between emission peaks (in km/s ?)
# line['wave']=[l, lref] eg. [3726.2, 3728.9] # Observed or Rest;
# line['ratio']=[0.8, 1.0] eg. [0.8, 1.0] ; the primary line for redshifts is the reddest
delta = 3e5 * (self.line['wave'][1]-self.line['wave'][0])/(self.line['wave'][0]+self.line['wave'][1])*2
ratio = self.line['ratio'][0] / self.line['ratio'][1]
# Add the blue doublet
finalcube = finalcube + ratio * np.exp(-0.5 * (vgrid - (vcube - delta)) ** 2 / (scube ** 2))
# delta = 217. # km/s OII
# delta = -945 # km/s Ha 6564.614 NII 6585.27
# delta = 2872 # km/s OIII 4960.295 5008.240
# delta = 641 # km/s SII 6718.29 6732.67
# Normalize from amplitude ratios int f dv = int f dz
ampsum = bn.nansum(finalcube,0)
#amplitude = f_map / bn.nansum(finalcube, 0)
#amplitude[np.isnan(amplitude)] = 0
amplitude = f_map / ampsum
amplitude[np.isfinite(amplitude)==False] = 0
#amplitude = np.where(np.isfinite(amplitude),f_map / ampsum, 0)
# Create a cube of Amplitude in each z plane
amplitude_cube = np.resize(amplitude, shape)
finalcube = finalcube * amplitude_cube
finalcube[np.isfinite(finalcube)==False] = 0
cube = HyperCube(data=finalcube)
return cube
def _compute_galaxy_model(self, galaxy, shape):
"""
Create a disk model in a cube
galaxy: GalaxyParameters
The parameters of the galaxy
Returns
3D flux cube,
3D Vz cube,
2D Vmap (1st moment of Vz),
2D S_map,
2D (2nd moment of Vz),
2D disk_dispersion,
2D intrinsic
"""
# Return indices in source frame; vz in image frame
hz = galaxy.radius * 0.15
# trick to go faster, as we know the max z shape the resulting model will fit in
# z_shape = np.ceil(shape[1] / 2.5 * 0.15 * 6.) ## 6 sigmas
# z_shape = np.ceil(self.max_boundaries.radius / 2.5 * 0.15 * 6.) ## 6 sigmas
# or
# z_shape = np.ceil(self.max_boundaries.radius * 2 * 3 * 0.15) # 3 hz_max on each side
# this short cut introduced a bug in the flux_deconvolved maps! fixed after 1.6.0
z_shape = int(np.ceil ( (shape[1] + shape[2]) / 2.)) #mean of spatial dimensions
self.logger.debug(" using z_shape %d in compute_galaxy_model" % (z_shape) )
self.logger.debug(" cube is %d in z- " % (shape[0]) )
# Minimum shape for z direction -- collapsed, so big sizes unimportant
# z_shape = 6 hz(sigma)_max;
# hz_max = size_box/2.5 (proxy for radius max) * 0.15
# Create arrays of indices in image frame; this cube has spatial dimentions in x,y,z; so should be almost a perfect cube in shape.
ind_shape = tuple([z_shape, shape[1], shape[2]])
z, y, x = np.indices(ind_shape)
#adding 0.5 to be centered on pixel center
nx, ny, nz, vz_cube, vtot = self._map_indices(x, y, z, hz, galaxy)
radius_cube = np.hypot(nx,ny)
flux_cube = self._set_flux_profile(galaxy, radius_cube)
flux_cube = self._set_thickness_profile(nz, hz, flux_cube)
# Flux weighted
flux_map = bn.nansum(flux_cube, 0)
vz_map = bn.nansum(vz_cube * flux_cube / flux_map, 0)
v2_map = bn.nansum(vz_cube ** 2 * flux_cube / flux_map, 0)
Var_map = v2_map - vz_map ** 2
sig_map_disk = np.sqrt(Var_map)
### Add Sigma_Aleatoire_disk
sig_ale_disk = self._set_dispersion_profile(hz, vtot, radius_cube)
# Compute weighted mean for Sig_disk
# a) 2D map = flux-weighted map
#Sigz_disk_map = bn.nansum(Sig_disk*A,0)/Flux_map
# b) 2D map = flux-weighted map rms
#sigz_map_ale = np.sqrt(bn.nansum(sig_ale_disk**2*A,0)/Flux_map)
# c) Normalized with flux squared
sigz_map_ale = np.sqrt(bn.nansum(sig_ale_disk ** 2 * flux_cube ** 2, 0) / bn.nansum(flux_cube * flux_cube, 0))
# Intrinsic Dispersion
sig_intr = np.ones_like(sigz_map_ale) * galaxy.sigma0
# Total S-map
s_map = np.sqrt(sig_map_disk ** 2 + sigz_map_ale ** 2 + sig_intr ** 2)
# Normalize by flux
total = bn.nansum(flux_cube)
flux_cube = np.where(total>0, flux_cube / total, 0)
return flux_cube, vz_cube, vz_map, s_map, \
sig_map_disk, sigz_map_ale, sig_intr
def _map_indices(self, xx, yy, zz, hz, galaxy ):
"""
Takes arrays of indices, galaxy parameters, and rotates accordingly.
Returns xx, yy, zz indices of object in image frame
Returns z-component of velocity of object in image frame
"""
inclination = np.radians(galaxy.inclination)
pa = np.radians(galaxy.pa - 90)
zo = (np.size(zz, 0) - 1) / 2. # center along z
# Rotation inclination (around x)
rot_x = lambda i: np.array([[1, 0, 0], [0, np.cos(i), -np.sin(i)], [0, np.sin(i), np.cos(i)]])
rot_i = rot_x(inclination)
rot_mi = rot_x(-inclination)
# Rotation PA (around z) anti-clockwise
rot_z = lambda a: np.array([[np.cos(a), np.sin(a), 0], [-np.sin(a), np.cos(a), 0], [0, 0, 1]])
rot_pa = rot_z(pa)
rot_mpa = rot_z(-pa)
# Transformation 1 from sky coord to disk plane
xx = xx - galaxy.x
yy = yy - galaxy.y
zz = zz - zo
# # Rotation around z for PA
# nx = rot_pa[0, 0] * xx + rot_pa[0, 1] * yy
# ny = rot_pa[1, 0] * xx + rot_pa[1, 1] * yy
# nz = rot_pa[2, 2] * zz
# # Rotation around x for inclination
# x = rot_i[0, 0] * nx
# y = rot_i[1, 1] * ny + rot_i[1, 2] * nz
# z = rot_i[2, 1] * ny + rot_i[2, 2] * nz
coord = np.array([xx, yy, zz])
rot = np.linalg.multi_dot([rot_i, rot_pa, coord.reshape(3, -1)])
x, y, z = rot.reshape(coord.shape)
# Compute the radius cube
# radius_cube, _ = self._set_flux_profile(galaxy, x, y, z)
radius_cube = np.hypot(x, y)
# Circular orbits -- we're DIVIDING BY ZERO ; it's okay, need for speed
vx = y / radius_cube
vy = - x / radius_cube
vx[~np.isfinite(vx)] = 0
vy[~np.isfinite(vy)] = 0
v_profile = self._set_velocity_profile(galaxy, radius_cube)
# Remove NaNs where radius is 0
v_profile[radius_cube == 0] = 0.
# 3D velocity vectors
vx *= v_profile
vy *= v_profile
# # Rotation of -inclination around x
# vvx = rot_mi[0, 0] * vx
# vvy = rot_mi[1, 1] * vy
# vvz = rot_mi[2, 1] * vy
# # Rotation of -pa around z
# vx = rot_mpa[0, 0] * vvx + rot_mpa[0, 1] * vvy
# vy = rot_mpa[1, 0] * vvx + rot_mpa[1, 1] * vvy
# vz = rot_mpa[2, 2] * vvz
# v = np.sqrt(vx ** 2 + vy ** 2 + vz ** 2)
coord = np.array([vx, vy, vy]).reshape(3, -1)
coord *= rot_mi[[0,1,2], [0,1,1]][:, None]
rot = np.dot(rot_mpa, coord)
vz = rot[2].reshape(vx.shape)
v = np.sqrt(np.sum(rot**2, axis=0))
v = v.reshape(vx.shape)
return x, y, z, vz, v
####################################
def _fv_newton(self, r, radius, vmax):
"""
Returns velocity given the r-cube
The cumulative flux I(<r) profile is analytically calculated given the profile shape
"""
mass = 1 # enclosed_mass for Gaussian
if self.__dict__['flux_profile'] == 'de_vaucouleurs':
#(-4 (5040 + 5040 x^(1/4) + 2520 Sqrt[x] + 840 x^(3/4) + 210 x + 42 x^(5/4) + 7 x^(3/2) + x^(7/4)))/E^x^(1/4)
int_x_dev = lambda x: (-4. * (5040. + 5040. * x ** (1. / 4) + 2520. * np.sqrt(x) + 840. * x ** (3. / 4) + 210. * x + 42. * x ** (5. / 4) + 7. * x ** (3. / 2) + x ** (7. / 4))) * np.exp(-x ** (1. / 4))
mtot = mass * (int_x_dev(r / radius) - int_x_dev(0))
elif self.__dict__['flux_profile'] == 'gaussian':
mtot = mass * (1. - np.exp(-r ** 2 / 2. / (2 * radius / 2.35) ** 2))
elif self.__dict__['flux_profile'] == 'exponential':
#was wrong:
#mtot = mass * (1. - ( r / radius) * np.exp(- r / radius) - np.exp(- r / radius))
#should be:
mtot = mass * (1. - (1.68 * r / radius) * np.exp(-1.68 * r / radius) - np.exp(-1.68 * r / radius))
else:
raise NotImplementedError("Flux profile not supported. Should be '%s' " %
self.model.FLUX_VALID)
v = np.sqrt(mtot / (r + 1e-9)) # km/s
# Normalization set by Vmax (sets the mass)
if np.size(v) > 1:
v = v / bn.nanmax(v) * vmax
else:
v = v / np.max(v) * vmax
return v
def _compute_Mdyn_at_Rhalf(self, galaxy, radius=None): #, flux_profile, rotation_curve):
"""
From galaxy, computes V profile and dynamic_mass
radius: float
In pixels, the half-light radius
"""
rv, max_vel = galaxy['turnover_radius'], galaxy['maximum_velocity']
if radius is None:
radius = galaxy['radius']
try:
from astropy.cosmology import WMAP5
except ImportError:
raise ImportError('The package `astropy` is required.')
if self.redshift is None:
raise RuntimeError("Cannot compute mass if redshift is not provided.")
redshift = self.redshift
pixscale = self.pixscale
# Compute angular distance in Mpc
dang = WMAP5.angular_diameter_distance(redshift).value
# Convert it to kpc/arcsec
dang *= 1e3 * np.radians(1. / 3600)
rhalf_kpc = radius * pixscale * dang # in kpc
r_kpc = np.r_[0:200:100j] # in kpc
r = r_kpc / dang # in arcsec
r_pix = r / pixscale # in pixel
if self.__dict__['rotation_curve'] == 'arctan':
fv = lambda q: np.arctan(q / rv) * max_vel * 2. / np.pi # r in pix
elif self.__dict__['rotation_curve'] == 'mass':
s = r_pix
fv = lambda q: self._fv_newton(q, s, max_vel) # r in pix
elif self.__dict__['rotation_curve'] == 'exponential': # Feng inv_exp profile
fv = lambda q: max_vel * (1.0 - np.exp(-q / rv))
elif self.__dict__['rotation_curve'] == 'tanh' :
fv = lambda q: max_vel * np.tanh(q / rv)
elif self.__dict__['rotation_curve'] == 'isothermal':
fv = lambda q: max_vel * np.sqrt(1-rv/r*np.arctan(r/rv))
else:
raise ValueError("Unsupported Rotational Curve. Should be '%s'" % self.model.CURVE_VALID)
# v² = (GM/r) in km/s
compute_mass = lambda vol, rad: (1e3 * vol) ** 2 * (PARSEC * rad * 1e3) / G / SOL_MASS
v_profile = fv(r_pix)
v_rhalf = interpolate.spline(r_kpc, v_profile, rhalf_kpc)
dynamic_mass = compute_mass(v_rhalf, rhalf_kpc)#computed at R1/2.
#mass_profile = compute_mass(v_profile, r_kpc)
#v_2rhalf = interpolate.spline(r_kpc, v_profile, 2. * rhalf_kpc)
#rvir_kpc = 180 # compute_Rvir(Vmax)
#Vvir = interpolate.spline(r_kpc, v_profile, rvir_kpc)
#Mvir = 0.83 * (max_vel / 200.) ** 3 * (1 + context['redshift']) ** 1.5 * 1e12
return dynamic_mass
### STATIC METHODS #########################################################
@staticmethod
def flux_gaussian(nx, ny, nz, rhwhm):
"""
Disk Gaussian profile with Gaussian thickness
"""
radius = np.sqrt(nx ** 2 + ny ** 2)
energy = np.exp(-radius ** 2 / 2. / (2. * rhwhm / 2.35) ** 2) #* np.exp(-nz ** 2 / 2. / hz ** 2)
return radius, energy
@staticmethod
def flux_sersic(radius, size, index):
"""
Sersic profile with Gaussian thickness
See http://en.wikipedia.org/wiki/Sersic_profile
"""
beta = 1.9992 * index - 0.3271
energy = np.exp(-beta * (radius / size) ** (1. / index))
#energy = np.exp(-beta * (radius / size) ** (1. / index)) * np.exp(-nz ** 2 / 2. / hz ** 2)
#energy = np.exp(-beta * (radius / size) ** (1. / index)) * np.exp(-np.sqrt(nz ** 2) / hz ) #exponential z-profile
#energy = np.exp(-beta * (radius / size) ** (1. / index)) * np.cosh(-nz / hz )**(-2.) #sech^2 z-profile
return energy
@staticmethod
def flux_exponential(radius, size):
"""
Disk exponential profile with Gaussian thickness
This is merely a Sérsic profile with index = 1
"""
return DiskModel.flux_sersic(radius, size, 1.)
@staticmethod
def flux_de_vaucouleurs(radius, size):
"""
De Vaucouleurs profile with Gaussian thickness
See http://en.wikipedia.org/wiki/De_Vaucouleurs%27_law
This is merely a Sérsic profile with index = 4
"""
return DiskModel.flux_sersic(radius, size, 4.)
def __str__(self):
dic = vars(self)
dic['model'] = self.__name__()
return str(dic)
def __name__(self):
return self.__class__.__name__