# The DF of a gap in a tidal stream
from functools import wraps
import copy
import warnings
import multiprocessing
import numpy
from scipy import integrate, interpolate, special
from ..util import galpyWarning, coords, multi, conversion
from ..util import _rotate_to_arbitrary_vector
from ..orbit import Orbit
from ..potential import evaluateRforces, MovingObjectPotential, \
PlummerPotential
from .df import df
from ..util.conversion import physical_conversion
from . import streamdf
from .streamdf import _determine_stream_track_single
from ..potential import flatten as flatten_potential
def impact_check_range(func):
"""Decorator to check the range of interpolated kicks"""
@wraps(func)
def impact_wrapper(*args,**kwargs):
if isinstance(args[1],numpy.ndarray):
out= numpy.zeros(len(args[1]))
goodIndx= (args[1] < args[0]._deltaAngleTrackImpact)*(args[1] > 0.)
out[goodIndx]= func(args[0],args[1][goodIndx])
return out
elif args[1] >= args[0]._deltaAngleTrackImpact or args[1] <= 0.:
return 0.
else:
return func(*args,**kwargs)
return impact_wrapper
[docs]class streamgapdf(streamdf.streamdf):
"""The DF of a gap in a tidal stream"""
[docs] def __init__(self,*args,**kwargs):
"""
NAME:
__init__
PURPOSE:
Initialize the DF of a gap in a stellar stream
INPUT:
streamdf args and kwargs
Subhalo and impact parameters:
impactb= impact parameter (can be Quantity)
subhalovel= velocity of the subhalo shape=(3) (can be Quantity)
timpact time since impact (can be Quantity)
impact_angle= angle offset from progenitor at which the impact occurred (rad) (can be Quantity)
Subhalo: specify either 1( mass and size of Plummer sphere or 2( general spherical-potential object (kick is numerically computed)
1( GM= mass of the subhalo (can be Quantity)
rs= size parameter of the subhalo (can be Quantity)
2( subhalopot= galpy potential object or list thereof (should be spherical)
3( hernquist= (False) if True, use Hernquist kicks for GM/rs
deltaAngleTrackImpact= (None) angle to estimate the stream track over to determine the effect of the impact [similar to deltaAngleTrack] (rad)
nTrackChunksImpact= (floor(deltaAngleTrack/0.15)+1) number of chunks to divide the progenitor track in near the impact [similar to nTrackChunks]
nKickPoints= (30xnTrackChunksImpact) number of points along the stream to compute the kicks at (kicks are then interpolated); '30' chosen such that higherorderTrack can be set to False and get calculations accurate to > 99%
nokicksetup= (False) if True, only run as far as setting up the coordinate transformation at the time of impact (useful when using this in streampepperdf)
spline_order= (3) order of the spline to interpolate the kicks with
higherorderTrack= (False) if True, calculate the track using higher-order terms
OUTPUT:
object
HISTORY:
2015-06-02 - Started - Bovy (IAS)
"""
df.__init__(self,ro=kwargs.get('ro',None),vo=kwargs.get('vo',None))
# Parse kwargs
impactb= conversion.parse_length(kwargs.pop('impactb',1.),ro=self._ro)
subhalovel= conversion.parse_velocity(\
kwargs.pop('subhalovel',numpy.array([0.,1.,0.])),
vo=self._vo)
hernquist= kwargs.pop('hernquist',False)
GM= kwargs.pop('GM',None)
if not GM is None:
GM= conversion.parse_mass(GM,ro=self._ro,vo=self._vo)
rs= kwargs.pop('rs',None)
if not rs is None:
rs= conversion.parse_length(rs,ro=self._ro)
subhalopot= kwargs.pop('subhalopot',None)
timpact= conversion.parse_time(kwargs.pop('timpact',1.),
ro=self._ro,vo=self._vo)
impact_angle= conversion.parse_angle(kwargs.pop('impact_angle',1.))
nokicksetup= kwargs.pop('nokicksetup',False)
deltaAngleTrackImpact= kwargs.pop('deltaAngleTrackImpact',None)
nTrackChunksImpact= kwargs.pop('nTrackChunksImpact',None)
nKickPoints= kwargs.pop('nKickPoints',None)
spline_order= kwargs.pop('spline_order',3)
higherorderTrack= kwargs.pop('higherorderTrack',False)
# For setup later
nTrackChunks= kwargs.pop('nTrackChunks',None)
interpTrack= kwargs.pop('interpTrack',
streamdf._INTERPDURINGSETUP)
useInterp= kwargs.pop('useInterp',
streamdf._USEINTERP)
# Analytical Plummer or general potential?
self._general_kick= GM is None or rs is None
if self._general_kick and subhalopot is None:
raise IOError("One of (GM=, rs=) or subhalopot= needs to be set to specify the subhalo's structure")
# Now run the regular streamdf setup, but without calculating the
# stream track (nosetup=True)
kwargs['nosetup']= True
super(streamgapdf,self).__init__(*args,**kwargs)
# Setup the machinery to go between (x,v) and (Omega,theta)
# near the impact
self._determine_nTrackIterations(kwargs.get('nTrackIterations',None))
self._determine_deltaAngleTrackImpact(deltaAngleTrackImpact,timpact)
self._determine_impact_coordtransform(self._deltaAngleTrackImpact,
nTrackChunksImpact,
timpact,impact_angle)
# Set nKickPoints
if nKickPoints is None:
self._nKickPoints= 30*self._nTrackChunksImpact
else:
self._nKickPoints= nKickPoints
if nokicksetup: return None
# Compute \Delta Omega ( \Delta \theta_perp) and \Delta theta,
# setup interpolating function
self._determine_deltav_kick(impact_angle,impactb,subhalovel,
GM,rs,subhalopot,
spline_order,hernquist)
self._determine_deltaOmegaTheta_kick(spline_order)
# Then pass everything to the normal streamdf setup
self.nInterpolatedTrackChunks= 201 #more expensive now
self._higherorderTrack= higherorderTrack
super(streamgapdf,self)._determine_stream_track(nTrackChunks)
self._useInterp= useInterp
if interpTrack or self._useInterp:
super(streamgapdf,self)._interpolate_stream_track()
super(streamgapdf,self)._interpolate_stream_track_aA()
super(streamgapdf,self).calc_stream_lb()
return None
def pOparapar(self,Opar,apar):
"""
NAME:
pOparapar
PURPOSE:
return the probability of a given parallel (frequency,angle) offset pair
INPUT:
Opar - parallel frequency offset (array) (can be Quantity)
apar - parallel angle offset along the stream (scalar) (can be Quantity)
OUTPUT:
p(Opar,apar)
HISTORY:
2015-11-17 - Written - Bovy (UofT)
"""
Opar= conversion.parse_frequency(Opar,ro=self._ro,vo=self._vo)
apar= conversion.parse_angle(apar)
if isinstance(Opar,(int,float,numpy.float32,numpy.float64)):
Opar= numpy.array([Opar])
out= numpy.zeros(len(Opar))
# Compute ts and where they were at impact for all
ts= apar/Opar
apar_impact= apar-Opar*self._timpact
dOpar_impact= self._kick_interpdOpar(apar_impact)
Opar_b4impact= Opar-dOpar_impact
# Evaluate the smooth model in the two regimes:
# stripped before or after impact
afterIndx= (ts < self._timpact)*(ts >= 0.)
out[afterIndx]=\
super(streamgapdf,self).pOparapar(Opar[afterIndx],
apar)
out[True^afterIndx]=\
super(streamgapdf,self).pOparapar(Opar_b4impact[True^afterIndx],
apar_impact[True^afterIndx],
tdisrupt=
self._tdisrupt-self._timpact)
return out
def _density_par(self,dangle,tdisrupt=None,approx=True,
higherorder=None):
"""The raw density as a function of parallel angle,
approx= use faster method that directly integrates the spline
representation"""
if higherorder is None: higherorder= self._higherorderTrack
if tdisrupt is None: tdisrupt= self._tdisrupt
if approx:
return self._density_par_approx(dangle,tdisrupt,
higherorder=higherorder)
else:
return integrate.quad(lambda T: numpy.sqrt(self._sortedSigOEig[2])\
*(1+T*T)/(1-T*T)**2.\
*self.pOparapar(T/(1-T*T)\
*numpy.sqrt(self._sortedSigOEig[2])\
+self._meandO,dangle),
-1.,1.)[0]
def _density_par_approx(self,dangle,tdisrupt,_return_array=False,
higherorder=False):
"""Compute the density as a function of parallel angle using the
spline representation + approximations"""
# First construct the breakpoints for this dangle
Oparb= (dangle-self._kick_interpdOpar_poly.x)/self._timpact
# Find the lower limit of the integration in the pw-linear-kick approx.
lowbindx,lowx= self.minOpar(dangle,tdisrupt,_return_raw=True)
lowbindx= numpy.arange(len(Oparb)-1)[lowbindx]
Oparb[lowbindx+1]= Oparb[lowbindx]-lowx
# Now integrate between breakpoints
out= (0.5/(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)\
*(special.erf(1./numpy.sqrt(2.*self._sortedSigOEig[2])\
*(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO))\
-special.erf(1./numpy.sqrt(2.*self._sortedSigOEig[2])*(numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO\
-self._kick_interpdOpar_poly.c[-2]*self._timpact*(Oparb-numpy.roll(Oparb,-1))[:-1]))))
if _return_array:
return out
out= numpy.sum(out[:lowbindx+1])
if higherorder:
# Add higher-order contribution
out+= self._density_par_approx_higherorder(Oparb,lowbindx)
# Add integration to infinity
out+= 0.5*(1.+special.erf((self._meandO-Oparb[0])\
/numpy.sqrt(2.*self._sortedSigOEig[2])))
return out
def _density_par_approx_higherorder(self,Oparb,lowbindx,
_return_array=False,
gaussxpolyInt=None):
"""Contribution from non-linear spline terms"""
spline_order= self._kick_interpdOpar_raw._eval_args[2]
if spline_order == 1: return 0.
# Form all Gaussian-like integrals necessary
ll= (numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]\
-self._meandO\
-self._kick_interpdOpar_poly.c[-2]*self._timpact\
*(Oparb-numpy.roll(Oparb,-1))[:-1])\
/numpy.sqrt(2.*self._sortedSigOEig[2])
ul= (Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)\
/numpy.sqrt(2.*self._sortedSigOEig[2])
if gaussxpolyInt is None:
gaussxpolyInt=\
self._densMoments_approx_higherorder_gaussxpolyInts(\
ll,ul,spline_order+1)
# Now multiply in the coefficients for each order
powers= numpy.tile(numpy.arange(spline_order+1)[::-1],
(len(ul),1)).T
gaussxpolyInt*= -0.5*(-numpy.sqrt(2.))**(powers+1)\
*self._sortedSigOEig[2]**(0.5*(powers-1))
powers= numpy.tile(numpy.arange(spline_order+1)[::-1][:-2],
(len(ul),1)).T
for jj in range(spline_order+1):
gaussxpolyInt[-jj-1]*= numpy.sum(\
self._kick_interpdOpar_poly.c[:-2]
*self._timpact**powers
/(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)
**(powers+1)
*special.binom(powers,jj)
*(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)
**(powers-jj),axis=0)
if _return_array:
return numpy.sum(gaussxpolyInt,axis=0)
else:
return numpy.sum(gaussxpolyInt[:,:lowbindx+1])
def _densMoments_approx_higherorder_gaussxpolyInts(self,ll,ul,maxj):
"""Calculate all of the polynomial x Gaussian integrals occuring
in the higher-order terms, recursively"""
gaussxpolyInt= numpy.zeros((maxj,len(ul)))
gaussxpolyInt[-1]= 1./numpy.sqrt(numpy.pi)\
*(numpy.exp(-ll**2.)-numpy.exp(-ul**2.))
gaussxpolyInt[-2]= 1./numpy.sqrt(numpy.pi)\
*(numpy.exp(-ll**2.)*ll-numpy.exp(-ul**2.)*ul)\
+0.5*(special.erf(ul)-special.erf(ll))
for jj in range(maxj-2):
gaussxpolyInt[-jj-3]= 1./numpy.sqrt(numpy.pi)\
*(numpy.exp(-ll**2.)*ll**(jj+2)-numpy.exp(-ul**2.)*ul**(jj+2))\
+0.5*(jj+2)*gaussxpolyInt[-jj-1]
return gaussxpolyInt
def minOpar(self,dangle,tdisrupt=None,_return_raw=False):
"""
NAME:
minOpar
PURPOSE:
return the approximate minimum parallel frequency at a given angle
INPUT:
dangle - parallel angle
OUTPUT:
minimum frequency that gets to this parallel angle
HISTORY:
2015-12-28 - Written - Bovy (UofT)
"""
if tdisrupt is None: tdisrupt= self._tdisrupt
# First construct the breakpoints for this dangle
Oparb= (dangle-self._kick_interpdOpar_poly.x[:-1])/self._timpact
# Find the lower limit of the integration in the pw-linear-kick approx.
lowx= ((Oparb-self._kick_interpdOpar_poly.c[-1])\
*(tdisrupt-self._timpact)+Oparb*self._timpact-dangle)\
/((tdisrupt-self._timpact)\
*(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)\
+self._timpact)
lowx[lowx < 0.]= numpy.inf
lowbindx= numpy.argmin(lowx)
if _return_raw:
return (lowbindx,lowx[lowbindx])
else:
return Oparb[lowbindx]-lowx[lowbindx]
@physical_conversion('frequency',pop=True)
def meanOmega(self,dangle,oned=False,tdisrupt=None,approx=True,
higherorder=None):
"""
NAME:
meanOmega
PURPOSE:
calculate the mean frequency as a function of angle, assuming a uniform time distribution up to a maximum time
INPUT:
dangle - angle offset
oned= (False) if True, return the 1D offset from the progenitor (along the direction of disruption)
approx= (True) if True, compute the mean Omega by direct integration of the spline representation
higherorder= (object-wide default higherorderTrack) if True, include higher-order spline terms in the approximate computation
OUTPUT:
mean Omega
HISTORY:
2015-11-17 - Written - Bovy (UofT)
"""
if higherorder is None: higherorder= self._higherorderTrack
if tdisrupt is None: tdisrupt= self._tdisrupt
if approx:
num= self._meanOmega_num_approx(dangle,tdisrupt,
higherorder=higherorder)
else:
num=\
integrate.quad(lambda T: (T/(1-T*T)\
*numpy.sqrt(self._sortedSigOEig[2])\
+self._meandO)\
*numpy.sqrt(self._sortedSigOEig[2])\
*(1+T*T)/(1-T*T)**2.\
*self.pOparapar(T/(1-T*T)\
*numpy.sqrt(self._sortedSigOEig[2])\
+self._meandO,dangle),
-1.,1.)[0]
denom= self._density_par(dangle,tdisrupt=tdisrupt,approx=approx,
higherorder=higherorder)
dO1D= num/denom
if oned: return dO1D
else:
return self._progenitor_Omega+dO1D*self._dsigomeanProgDirection\
*self._sigMeanSign
def _meanOmega_num_approx(self,dangle,tdisrupt,higherorder=False):
"""Compute the numerator going into meanOmega using the direct integration of the spline representation"""
# First construct the breakpoints for this dangle
Oparb= (dangle-self._kick_interpdOpar_poly.x)/self._timpact
# Find the lower limit of the integration in the pw-linear-kick approx.
lowbindx,lowx= self.minOpar(dangle,tdisrupt,_return_raw=True)
lowbindx= numpy.arange(len(Oparb)-1)[lowbindx]
Oparb[lowbindx+1]= Oparb[lowbindx]-lowx
# Now integrate between breakpoints
out= numpy.sum(((Oparb[:-1]
+(self._meandO+self._kick_interpdOpar_poly.c[-1]
-Oparb[:-1])/
(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact))
*self._density_par_approx(dangle,tdisrupt,
_return_array=True)
+numpy.sqrt(self._sortedSigOEig[2]/2./numpy.pi)/
(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)**2.
*(numpy.exp(-0.5*(Oparb[:-1]
-self._kick_interpdOpar_poly.c[-1]
-(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)
*(Oparb-numpy.roll(Oparb,-1))[:-1]
-self._meandO)**2.
/self._sortedSigOEig[2])
-numpy.exp(-0.5*(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]
-self._meandO)**2.
/self._sortedSigOEig[2])))[:lowbindx+1])
if higherorder:
# Add higher-order contribution
out+= self._meanOmega_num_approx_higherorder(Oparb,lowbindx)
# Add integration to infinity
out+= 0.5*(numpy.sqrt(2./numpy.pi)*numpy.sqrt(self._sortedSigOEig[2])\
*numpy.exp(-0.5*(self._meandO-Oparb[0])**2.\
/self._sortedSigOEig[2])
+self._meandO
*(1.+special.erf((self._meandO-Oparb[0])
/numpy.sqrt(2.*self._sortedSigOEig[2]))))
return out
def _meanOmega_num_approx_higherorder(self,Oparb,lowbindx):
"""Contribution from non-linear spline terms"""
spline_order= self._kick_interpdOpar_raw._eval_args[2]
if spline_order == 1: return 0.
# Form all Gaussian-like integrals necessary
ll= (numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]\
-self._meandO\
-self._kick_interpdOpar_poly.c[-2]*self._timpact\
*(Oparb-numpy.roll(Oparb,-1))[:-1])\
/numpy.sqrt(2.*self._sortedSigOEig[2])
ul= (Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)\
/numpy.sqrt(2.*self._sortedSigOEig[2])
gaussxpolyInt=\
self._densMoments_approx_higherorder_gaussxpolyInts(ll,ul,
spline_order+2)
firstTerm= Oparb[:-1]\
*self._density_par_approx_higherorder(\
Oparb,lowbindx,_return_array=True,
gaussxpolyInt=copy.copy(gaussxpolyInt[1:]))
# Now multiply in the coefficients for each order
powers= numpy.tile(numpy.arange(spline_order+2)[::-1],
(len(ul),1)).T
gaussxpolyInt*= -0.5*(-numpy.sqrt(2.))**(powers+1)\
*self._sortedSigOEig[2]**(0.5*(powers-1))
powers= numpy.tile(numpy.arange(spline_order+1)[::-1][:-2],
(len(ul),1)).T
for jj in range(spline_order+2):
gaussxpolyInt[-jj-1]*= numpy.sum(\
self._kick_interpdOpar_poly.c[:-2]
*self._timpact**powers
/(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)
**(powers+2)
*special.binom(powers+1,jj)
*(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)
**(powers-jj+1),axis=0)
out= numpy.sum(gaussxpolyInt,axis=0)
out+= firstTerm
return numpy.sum(out[:lowbindx+1])
def _determine_deltav_kick(self,impact_angle,impactb,subhalovel,
GM,rs,subhalopot,
spline_order,hernquist):
# Store some impact parameters
self._impactb= impactb
self._subhalovel= subhalovel
# Sign of delta angle tells us whether the impact happens to the
# leading or trailing arm, self._sigMeanSign contains this info;
# Checked before, but check it again in case impact_angle has changed
if impact_angle > 0.:
self._gap_leading= True
else:
self._gap_leading= False
if (self._gap_leading and not self._leading) \
or (not self._gap_leading and self._leading):
raise ValueError('Modeling leading (trailing) impact for trailing (leading) arm; this is not allowed because it is nonsensical in this framework')
self._impact_angle= numpy.fabs(impact_angle)
# Interpolate the track near the gap in (x,v) at the kick_thetas
self._interpolate_stream_track_kick()
self._interpolate_stream_track_kick_aA()
# Then compute delta v along the track
if self._general_kick:
self._kick_deltav=\
impulse_deltav_general_curvedstream(self._kick_interpolatedObsTrackXY[:,3:],
self._kick_interpolatedObsTrackXY[:,:3],
self._impactb,
self._subhalovel,
self._kick_ObsTrackXY_closest[:3],
self._kick_ObsTrackXY_closest[3:],
subhalopot)
else:
if hernquist:
deltav_func= impulse_deltav_hernquist_curvedstream
else:
deltav_func= impulse_deltav_plummer_curvedstream
self._kick_deltav= \
deltav_func(self._kick_interpolatedObsTrackXY[:,3:],
self._kick_interpolatedObsTrackXY[:,:3],
self._impactb,
self._subhalovel,
self._kick_ObsTrackXY_closest[:3],
self._kick_ObsTrackXY_closest[3:],
GM,rs)
return None
def _determine_deltaOmegaTheta_kick(self,spline_order):
# Propagate deltav(angle) -> delta (Omega,theta) [angle]
# Cylindrical coordinates of the perturbed points
vXp= self._kick_interpolatedObsTrackXY[:,3]+self._kick_deltav[:,0]
vYp= self._kick_interpolatedObsTrackXY[:,4]+self._kick_deltav[:,1]
vZp= self._kick_interpolatedObsTrackXY[:,5]+self._kick_deltav[:,2]
vRp,vTp,vZp=\
coords.rect_to_cyl_vec(vXp,vYp,vZp,
self._kick_interpolatedObsTrack[:,0],
self._kick_interpolatedObsTrack[:,5],
self._kick_interpolatedObsTrack[:,3],
cyl=True)
# We will abuse streamdf functions for doing the (O,a) -> (R,vR)
# coordinate transformation, to do this, we assign some of the
# attributes related to the track near the impact to the equivalent
# attributes related to the track at the present time, carefully
# removing this again to avoid confusion (as much as possible)
self._interpolatedObsTrack= self._kick_interpolatedObsTrack
self._ObsTrack= self._gap_ObsTrack
self._interpolatedObsTrackXY= self._kick_interpolatedObsTrackXY
self._ObsTrackXY= self._gap_ObsTrackXY
self._alljacsTrack= self._gap_alljacsTrack
self._interpolatedObsTrackAA= self._kick_interpolatedObsTrackAA
self._ObsTrackAA= self._gap_ObsTrackAA
self._nTrackChunks= self._nTrackChunksImpact
Oap= self._approxaA(self._kick_interpolatedObsTrack[:,0],
vRp,vTp,
self._kick_interpolatedObsTrack[:,3],
vZp,
self._kick_interpolatedObsTrack[:,5],interp=True,
cindx=range(len(self._kick_interpolatedObsTrackAA)))
# Remove attributes again to avoid confusion later
delattr(self,'_interpolatedObsTrack')
delattr(self,'_ObsTrack')
delattr(self,'_interpolatedObsTrackXY')
delattr(self,'_ObsTrackXY')
delattr(self,'_alljacsTrack')
delattr(self,'_interpolatedObsTrackAA')
delattr(self,'_ObsTrackAA')
delattr(self,'_nTrackChunks')
# Generate (dO,da)[angle_offset] and interpolate (raw here, see below
# for form that checks range)
self._kick_dOap= Oap.T-self._kick_interpolatedObsTrackAA
self._kick_interpdOr_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,0],
k=spline_order)
self._kick_interpdOp_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,1],
k=spline_order)
self._kick_interpdOz_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,2],
k=spline_order)
self._kick_interpdar_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,3],
k=spline_order)
self._kick_interpdap_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,4],
k=spline_order)
self._kick_interpdaz_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOap[:,5],
k=spline_order)
# Also interpolate parallel and perpendicular frequencies
self._kick_dOaparperp=\
numpy.dot(self._kick_dOap[:,:3],
self._sigomatrixEig[1][:,self._sigomatrixEigsortIndx])
self._kick_dOaparperp[:,2]*= self._sigMeanSign
self._kick_interpdOpar_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,
numpy.dot(self._kick_dOap[:,:3],self._dsigomeanProgDirection)\
*self._sigMeanSign,
k=spline_order) # to get zeros with sproot
self._kick_interpdOperp0_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOaparperp[:,0],
k=spline_order)
self._kick_interpdOperp1_raw=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,self._kick_dOaparperp[:,1],
k=spline_order)
# Also construct derivative of dOpar
self._kick_interpdOpar_dapar= self._kick_interpdOpar_raw.derivative(1)
# Also construct piecewise-polynomial representation of dOpar,
# removing intervals at the start and end with zero range
ppoly= interpolate.PPoly.from_spline(\
self._kick_interpdOpar_raw._eval_args)
nzIndx=\
numpy.nonzero((numpy.roll(ppoly.x,-1)-ppoly.x > 0)\
*(numpy.arange(len(ppoly.x)) < len(ppoly.x)//2)\
+(ppoly.x-numpy.roll(ppoly.x,1) > 0)\
*(numpy.arange(len(ppoly.x)) >= len(ppoly.x)//2))
self._kick_interpdOpar_poly= interpolate.PPoly(\
ppoly.c[:,nzIndx[0][:-1]],ppoly.x[nzIndx[0]])
return None
# Functions that evaluate the interpolated kicks, but also check the range
@impact_check_range
def _kick_interpdOpar(self,da):
return self._kick_interpdOpar_raw(da)
@impact_check_range
def _kick_interpdOperp0(self,da):
return self._kick_interpdOperp0_raw(da)
@impact_check_range
def _kick_interpdOperp1(self,da):
return self._kick_interpdOperp1_raw(da)
@impact_check_range
def _kick_interpdOr(self,da):
return self._kick_interpdOr_raw(da)
@impact_check_range
def _kick_interpdOp(self,da):
return self._kick_interpdOp_raw(da)
@impact_check_range
def _kick_interpdOz(self,da):
return self._kick_interpdOz_raw(da)
@impact_check_range
def _kick_interpdar(self,da):
return self._kick_interpdar_raw(da)
@impact_check_range
def _kick_interpdap(self,da):
return self._kick_interpdap_raw(da)
@impact_check_range
def _kick_interpdaz(self,da):
return self._kick_interpdaz_raw(da)
def _interpolate_stream_track_kick(self):
"""Build interpolations of the stream track near the kick"""
if hasattr(self,'_kick_interpolatedThetasTrack'): #pragma: no cover
self._store_closest()
return None #Already did this
# Setup the trackpoints where the kick will be computed, covering the
# full length of the stream
self._kick_interpolatedThetasTrack= \
numpy.linspace(self._gap_thetasTrack[0],
self._gap_thetasTrack[-1],
self._nKickPoints)
TrackX= self._gap_ObsTrack[:,0]*numpy.cos(self._gap_ObsTrack[:,5])
TrackY= self._gap_ObsTrack[:,0]*numpy.sin(self._gap_ObsTrack[:,5])
TrackZ= self._gap_ObsTrack[:,3]
TrackvX, TrackvY, TrackvZ=\
coords.cyl_to_rect_vec(self._gap_ObsTrack[:,1],
self._gap_ObsTrack[:,2],
self._gap_ObsTrack[:,4],
self._gap_ObsTrack[:,5])
#Interpolate
self._kick_interpTrackX=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackX,k=3)
self._kick_interpTrackY=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackY,k=3)
self._kick_interpTrackZ=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackZ,k=3)
self._kick_interpTrackvX=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackvX,k=3)
self._kick_interpTrackvY=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackvY,k=3)
self._kick_interpTrackvZ=\
interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack,
TrackvZ,k=3)
#Now store an interpolated version of the stream track
self._kick_interpolatedObsTrackXY= numpy.empty((len(self._kick_interpolatedThetasTrack),6))
self._kick_interpolatedObsTrackXY[:,0]=\
self._kick_interpTrackX(self._kick_interpolatedThetasTrack)
self._kick_interpolatedObsTrackXY[:,1]=\
self._kick_interpTrackY(self._kick_interpolatedThetasTrack)
self._kick_interpolatedObsTrackXY[:,2]=\
self._kick_interpTrackZ(self._kick_interpolatedThetasTrack)
self._kick_interpolatedObsTrackXY[:,3]=\
self._kick_interpTrackvX(self._kick_interpolatedThetasTrack)
self._kick_interpolatedObsTrackXY[:,4]=\
self._kick_interpTrackvY(self._kick_interpolatedThetasTrack)
self._kick_interpolatedObsTrackXY[:,5]=\
self._kick_interpTrackvZ(self._kick_interpolatedThetasTrack)
#Also in cylindrical coordinates
self._kick_interpolatedObsTrack= \
numpy.empty((len(self._kick_interpolatedThetasTrack),6))
tR,tphi,tZ= coords.rect_to_cyl(self._kick_interpolatedObsTrackXY[:,0],
self._kick_interpolatedObsTrackXY[:,1],
self._kick_interpolatedObsTrackXY[:,2])
tvR,tvT,tvZ=\
coords.rect_to_cyl_vec(self._kick_interpolatedObsTrackXY[:,3],
self._kick_interpolatedObsTrackXY[:,4],
self._kick_interpolatedObsTrackXY[:,5],
tR,tphi,tZ,cyl=True)
self._kick_interpolatedObsTrack[:,0]= tR
self._kick_interpolatedObsTrack[:,1]= tvR
self._kick_interpolatedObsTrack[:,2]= tvT
self._kick_interpolatedObsTrack[:,3]= tZ
self._kick_interpolatedObsTrack[:,4]= tvZ
self._kick_interpolatedObsTrack[:,5]= tphi
self._store_closest()
return None
def _store_closest(self):
# Also store (x,v) for the point of closest approach
self._kick_ObsTrackXY_closest= numpy.array([\
self._kick_interpTrackX(self._impact_angle),
self._kick_interpTrackY(self._impact_angle),
self._kick_interpTrackZ(self._impact_angle),
self._kick_interpTrackvX(self._impact_angle),
self._kick_interpTrackvY(self._impact_angle),
self._kick_interpTrackvZ(self._impact_angle)])
return None
def _interpolate_stream_track_kick_aA(self):
"""Build interpolations of the stream track near the impact in action-angle coordinates"""
if hasattr(self,'_kick_interpolatedObsTrackAA'): #pragma: no cover
return None #Already did this
#Calculate 1D meanOmega on a fine grid in angle and interpolate
dmOs= numpy.array([\
super(streamgapdf,self).meanOmega(da,oned=True,
tdisrupt=self._tdisrupt
-self._timpact,
use_physical=False)
for da in self._kick_interpolatedThetasTrack])
self._kick_interpTrackAAdmeanOmegaOneD=\
interpolate.InterpolatedUnivariateSpline(\
self._kick_interpolatedThetasTrack,dmOs,k=3)
#Build the interpolated AA
self._kick_interpolatedObsTrackAA=\
numpy.empty((len(self._kick_interpolatedThetasTrack),6))
for ii in range(len(self._kick_interpolatedThetasTrack)):
self._kick_interpolatedObsTrackAA[ii,:3]=\
self._progenitor_Omega+dmOs[ii]*self._dsigomeanProgDirection\
*self._gap_sigMeanSign
self._kick_interpolatedObsTrackAA[ii,3:]=\
self._progenitor_angle+self._kick_interpolatedThetasTrack[ii]\
*self._dsigomeanProgDirection*self._gap_sigMeanSign\
-self._timpact*self._progenitor_Omega
self._kick_interpolatedObsTrackAA[ii,3:]=\
numpy.mod(self._kick_interpolatedObsTrackAA[ii,3:],2.*numpy.pi)
return None
def _determine_deltaAngleTrackImpact(self,deltaAngleTrackImpact,timpact):
self._timpact= timpact
deltaAngleTrackLim = (self._sigMeanOffset+4.) * numpy.sqrt(
self._sortedSigOEig[2]) * (self._tdisrupt-self._timpact)
if deltaAngleTrackImpact is None:
deltaAngleTrackImpact= deltaAngleTrackLim
else:
if deltaAngleTrackImpact > deltaAngleTrackLim:
warnings.warn("WARNING: deltaAngleTrackImpact angle range large compared to plausible value", galpyWarning)
self._deltaAngleTrackImpact= deltaAngleTrackImpact
return None
def _determine_impact_coordtransform(self,deltaAngleTrackImpact,
nTrackChunksImpact,
timpact,impact_angle):
"""Function that sets up the transformation between (x,v) and (O,theta)"""
# Integrate the progenitor backward to the time of impact
self._gap_progenitor_setup()
# Sign of delta angle tells us whether the impact happens to the
# leading or trailing arm, self._sigMeanSign contains this info
if impact_angle > 0.:
self._gap_leading= True
else:
self._gap_leading= False
if (self._gap_leading and not self._leading) \
or (not self._gap_leading and self._leading):
raise ValueError('Modeling leading (trailing) impact for trailing (leading) arm; this is not allowed because it is nonsensical in this framework')
self._gap_sigMeanSign= 1.
if (self._gap_leading and self._progenitor_Omega_along_dOmega/self._sigMeanSign < 0.) \
or (not self._gap_leading and self._progenitor_Omega_along_dOmega/self._sigMeanSign > 0.):
self._gap_sigMeanSign= -1.
# Determine how much orbital time is necessary for the progenitor's orbit at the time of impact to cover the part of the stream near the impact; we cover the whole leading (or trailing) part of the stream
if nTrackChunksImpact is None:
#default is floor(self._deltaAngleTrackImpact/0.15)+1
self._nTrackChunksImpact= int(numpy.floor(self._deltaAngleTrackImpact/0.15))+1
else:
self._nTrackChunksImpact= nTrackChunksImpact
if self._nTrackChunksImpact < 4: self._nTrackChunksImpact= 4
dt= self._deltaAngleTrackImpact\
/self._progenitor_Omega_along_dOmega\
/self._sigMeanSign*self._gap_sigMeanSign
self._gap_trackts= numpy.linspace(0.,2*dt,2*self._nTrackChunksImpact-1) #to be sure that we cover it
#Instantiate an auxiliaryTrack, which is an Orbit instance at the mean frequency of the stream, and zero angle separation wrt the progenitor; prog_stream_offset is the offset between this track and the progenitor at zero angle (same as in streamdf, but just done at the time of impact rather than the current time)
prog_stream_offset=\
_determine_stream_track_single(self._aA,
self._gap_progenitor,
self._timpact, # around the t of imp
self._progenitor_angle-self._timpact*self._progenitor_Omega,
self._gap_sigMeanSign,
self._dsigomeanProgDirection,
lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False),
0.) #angle = 0
auxiliaryTrack= Orbit(prog_stream_offset[3])
if dt < 0.:
self._gap_trackts= numpy.linspace(0.,-2.*dt,2*self._nTrackChunksImpact-1)
#Flip velocities before integrating
auxiliaryTrack= auxiliaryTrack.flip()
auxiliaryTrack.integrate(self._gap_trackts,self._pot)
if dt < 0.:
#Flip velocities again
auxiliaryTrack.orbit[...,1]= -auxiliaryTrack.orbit[...,1]
auxiliaryTrack.orbit[...,2]= -auxiliaryTrack.orbit[...,2]
auxiliaryTrack.orbit[...,4]= -auxiliaryTrack.orbit[...,4]
#Calculate the actions, frequencies, and angle for this auxiliary orbit
acfs= self._aA.actionsFreqs(auxiliaryTrack(0.),maxn=3,
use_physical=False)
auxiliary_Omega= numpy.array([acfs[3],acfs[4],acfs[5]]).reshape(3\
)
auxiliary_Omega_along_dOmega= \
numpy.dot(auxiliary_Omega,self._dsigomeanProgDirection)
# compute the transformation using _determine_stream_track_single
allAcfsTrack= numpy.empty((self._nTrackChunksImpact,9))
alljacsTrack= numpy.empty((self._nTrackChunksImpact,6,6))
allinvjacsTrack= numpy.empty((self._nTrackChunksImpact,6,6))
thetasTrack= numpy.linspace(0.,self._deltaAngleTrackImpact,
self._nTrackChunksImpact)
ObsTrack= numpy.empty((self._nTrackChunksImpact,6))
ObsTrackAA= numpy.empty((self._nTrackChunksImpact,6))
detdOdJps= numpy.empty((self._nTrackChunksImpact))
if self._multi is None:
for ii in range(self._nTrackChunksImpact):
multiOut= _determine_stream_track_single(self._aA,
auxiliaryTrack,
self._gap_trackts[ii]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), #this factor accounts for the difference in frequency between the progenitor and the auxiliary track, no timpact bc gap_tracks is relative to timpact
self._progenitor_angle-self._timpact*self._progenitor_Omega,
self._gap_sigMeanSign,
self._dsigomeanProgDirection,
lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False),
thetasTrack[ii])
allAcfsTrack[ii,:]= multiOut[0]
alljacsTrack[ii,:,:]= multiOut[1]
allinvjacsTrack[ii,:,:]= multiOut[2]
ObsTrack[ii,:]= multiOut[3]
ObsTrackAA[ii,:]= multiOut[4]
detdOdJps[ii]= multiOut[5]
else:
multiOut= multi.parallel_map(\
(lambda x: _determine_stream_track_single(self._aA,
auxiliaryTrack,
self._gap_trackts[x]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), #this factor accounts for the difference in frequency between the progenitor and the auxiliary track, no timpact bc gap_tracks is relative to timpact
self._progenitor_angle-self._timpact*self._progenitor_Omega,
self._gap_sigMeanSign,
self._dsigomeanProgDirection,
lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False),
thetasTrack[x])),
range(self._nTrackChunksImpact),
numcores=numpy.amin([self._nTrackChunksImpact,
multiprocessing.cpu_count(),
self._multi]))
for ii in range(self._nTrackChunksImpact):
allAcfsTrack[ii,:]= multiOut[ii][0]
alljacsTrack[ii,:,:]= multiOut[ii][1]
allinvjacsTrack[ii,:,:]= multiOut[ii][2]
ObsTrack[ii,:]= multiOut[ii][3]
ObsTrackAA[ii,:]= multiOut[ii][4]
detdOdJps[ii]= multiOut[ii][5]
#Repeat the track calculation using the previous track, to get closer to it
for nn in range(self.nTrackIterations):
if self._multi is None:
for ii in range(self._nTrackChunksImpact):
multiOut= _determine_stream_track_single(self._aA,
Orbit(ObsTrack[ii,:]),
0.,
self._progenitor_angle-self._timpact*self._progenitor_Omega,
self._gap_sigMeanSign,
self._dsigomeanProgDirection,
lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False),
thetasTrack[ii])
allAcfsTrack[ii,:]= multiOut[0]
alljacsTrack[ii,:,:]= multiOut[1]
allinvjacsTrack[ii,:,:]= multiOut[2]
ObsTrack[ii,:]= multiOut[3]
ObsTrackAA[ii,:]= multiOut[4]
detdOdJps[ii]= multiOut[5]
else:
multiOut= multi.parallel_map(\
(lambda x: _determine_stream_track_single(self._aA,Orbit(ObsTrack[x,:]),0.,
self._progenitor_angle-self._timpact*self._progenitor_Omega,
self._gap_sigMeanSign,
self._dsigomeanProgDirection,
lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False),
thetasTrack[x])),
range(self._nTrackChunksImpact),
numcores=numpy.amin([self._nTrackChunksImpact,
multiprocessing.cpu_count(),
self._multi]))
for ii in range(self._nTrackChunksImpact):
allAcfsTrack[ii,:]= multiOut[ii][0]
alljacsTrack[ii,:,:]= multiOut[ii][1]
allinvjacsTrack[ii,:,:]= multiOut[ii][2]
ObsTrack[ii,:]= multiOut[ii][3]
ObsTrackAA[ii,:]= multiOut[ii][4]
detdOdJps[ii]= multiOut[ii][5]
#Store the track
self._gap_thetasTrack= thetasTrack
self._gap_ObsTrack= ObsTrack
self._gap_ObsTrackAA= ObsTrackAA
self._gap_allAcfsTrack= allAcfsTrack
self._gap_alljacsTrack= alljacsTrack
self._gap_allinvjacsTrack= allinvjacsTrack
self._gap_detdOdJps= detdOdJps
self._gap_meandetdOdJp= numpy.mean(self._gap_detdOdJps)
self._gap_logmeandetdOdJp= numpy.log(self._gap_meandetdOdJp)
#Also calculate _ObsTrackXY in XYZ,vXYZ coordinates
self._gap_ObsTrackXY= numpy.empty_like(self._gap_ObsTrack)
TrackX= self._gap_ObsTrack[:,0]*numpy.cos(self._gap_ObsTrack[:,5])
TrackY= self._gap_ObsTrack[:,0]*numpy.sin(self._gap_ObsTrack[:,5])
TrackZ= self._gap_ObsTrack[:,3]
TrackvX, TrackvY, TrackvZ=\
coords.cyl_to_rect_vec(self._gap_ObsTrack[:,1],
self._gap_ObsTrack[:,2],
self._gap_ObsTrack[:,4],
self._gap_ObsTrack[:,5])
self._gap_ObsTrackXY[:,0]= TrackX
self._gap_ObsTrackXY[:,1]= TrackY
self._gap_ObsTrackXY[:,2]= TrackZ
self._gap_ObsTrackXY[:,3]= TrackvX
self._gap_ObsTrackXY[:,4]= TrackvY
self._gap_ObsTrackXY[:,5]= TrackvZ
return None
def _gap_progenitor_setup(self):
"""Setup an Orbit instance that's the progenitor integrated backwards"""
self._gap_progenitor= self._progenitor().flip() # new orbit, flip velocities
# Make sure we do not use physical coordinates
self._gap_progenitor.turn_physical_off()
# Now integrate backward in time until tdisrupt
ts= numpy.linspace(0.,self._tdisrupt,1001)
self._gap_progenitor.integrate(ts,self._pot)
# Flip its velocities, should really write a function for this
self._gap_progenitor.orbit[...,1]= -self._gap_progenitor.orbit[...,1]
self._gap_progenitor.orbit[...,2]= -self._gap_progenitor.orbit[...,2]
self._gap_progenitor.orbit[...,4]= -self._gap_progenitor.orbit[...,4]
return None
################################SAMPLE THE DF##################################
def _sample_aAt(self,n):
"""Sampling frequencies, angles, and times part of sampling, for stream with gap"""
# Use streamdf's _sample_aAt to generate unperturbed frequencies,
# angles
Om,angle,dt= super(streamgapdf,self)._sample_aAt(n)
# Now rewind angles by timpact, apply the kicks, and run forward again
dangle_at_impact= angle-numpy.tile(self._progenitor_angle.T,(n,1)).T\
-(Om-numpy.tile(self._progenitor_Omega.T,(n,1)).T)*self._timpact
dangle_par_at_impact= numpy.dot(dangle_at_impact.T,
self._dsigomeanProgDirection)\
*self._gap_sigMeanSign
# Calculate and apply kicks (points not yet released have zero kick)
dOr= self._kick_interpdOr(dangle_par_at_impact)
dOp= self._kick_interpdOp(dangle_par_at_impact)
dOz= self._kick_interpdOz(dangle_par_at_impact)
Om[0,:]+= dOr
Om[1,:]+= dOp
Om[2,:]+= dOz
angle[0,:]+=\
self._kick_interpdar(dangle_par_at_impact)+dOr*self._timpact
angle[1,:]+=\
self._kick_interpdap(dangle_par_at_impact)+dOp*self._timpact
angle[2,:]+=\
self._kick_interpdaz(dangle_par_at_impact)+dOz*self._timpact
return (Om,angle,dt)
[docs]def impulse_deltav_plummer(v,y,b,w,GM,rs):
"""
NAME:
impulse_deltav_plummer
PURPOSE:
calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream
INPUT:
v - velocity of the stream (nstar,3)
y - position along the stream (nstar)
b - impact parameter
w - velocity of the Plummer sphere (3)
GM - mass of the Plummer sphere (in natural units)
rs - size of the Plummer sphere
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-04-30 - Written based on Erkal's expressions - Bovy (IAS)
"""
if len(v.shape) == 1:
v= numpy.reshape(v,(1,3))
y= numpy.reshape(y,(1,1))
nv= v.shape[0]
# Build the rotation matrices and their inverse
rot= _rotation_vy(v)
rotinv= _rotation_vy(v,inv=True)
# Rotate the Plummer sphere's velocity to the stream frames
tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1)
# Use Denis' expressions
wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.)
wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1]
wmag2= wpar**2.+wperp**2.
wmag= numpy.sqrt(wmag2)
out= numpy.empty_like(v)
denom= wmag*((b**2.+rs**2.)*wmag2+wperp**2.*y**2.)
out[:,0]= (b*wmag2*tilew[:,2]/wperp-y*wpar*tilew[:,0])/denom
out[:,1]= -wperp**2.*y/denom
out[:,2]= -(b*wmag2*tilew[:,0]/wperp+y*wpar*tilew[:,2])/denom
# deal w/ perpendicular impacts
wperp0Indx= numpy.fabs(wperp) < 10.**-10.
out[wperp0Indx,0]= (b*wmag2[wperp0Indx]-y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,0])/denom[wperp0Indx]
out[wperp0Indx,2]=-(b*wmag2[wperp0Indx]+y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,2])/denom[wperp0Indx]
# Rotate back to the original frame
return 2.0*GM*numpy.sum(\
rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1)
[docs]def impulse_deltav_plummer_curvedstream(v,x,b,w,x0,v0,GM,rs):
"""
NAME:
impulse_deltav_plummer_curvedstream
PURPOSE:
calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
b - impact parameter
w - velocity of the Plummer sphere (3)
x0 - point of closest approach
v0 - velocity of point of closest approach
GM - mass of the Plummer sphere (in natural units)
rs - size of the Plummer sphere
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-05-04 - Written based on above - SANDERS
"""
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
b_ = b0+x-x0
w = w-v
wmag = numpy.sqrt(numpy.sum(w**2,axis=1))
bdotw=numpy.sum(b_*w,axis=1)/wmag
denom= wmag*(numpy.sum(b_**2,axis=1)+rs**2-bdotw**2)
denom = 1./denom
return -2.0*GM*((b_.T-bdotw*w.T/wmag)*denom).T
def HernquistX(s):
"""
Computes X function from equations (33) & (34) of Hernquist (1990)
"""
if(s<0.):
raise ValueError("s must be positive in Hernquist X function")
elif(s<1.):
return numpy.log((1+numpy.sqrt(1-s*s))/s)/numpy.sqrt(1-s*s)
elif(s==1.):
return 1.
else:
return numpy.arccos(1./s)/numpy.sqrt(s*s-1)
[docs]def impulse_deltav_hernquist(v,y,b,w,GM,rs):
"""
NAME:
impulse_deltav_hernquist
PURPOSE:
calculate the delta velocity to due an encounter with a Hernquist sphere in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream
INPUT:
v - velocity of the stream (nstar,3)
y - position along the stream (nstar)
b - impact parameter
w - velocity of the Hernquist sphere (3)
GM - mass of the Hernquist sphere (in natural units)
rs - size of the Hernquist sphere
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-08-13 SANDERS, using Wyn Evans calculation
"""
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
nv= v.shape[0]
# Build the rotation matrices and their inverse
rot= _rotation_vy(v)
rotinv= _rotation_vy(v,inv=True)
# Rotate the Plummer sphere's velocity to the stream frames
tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1)
wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.)
wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1]
wmag2= wpar**2.+wperp**2.
wmag= numpy.sqrt(wmag2)
B = numpy.sqrt(b**2.+wperp**2.*y**2./wmag2)
denom= wmag*(B**2-rs**2)
denom=1./denom
s = numpy.sqrt(2.*B/(rs+B))
HernquistXv=numpy.vectorize(HernquistX)
Xfac = 1.-2.*rs/(rs+B)*HernquistXv(s)
out= numpy.empty_like(v)
out[:,0]= (b*tilew[:,2]/wperp-y*wpar*tilew[:,0]/wmag2)*denom*Xfac
out[:,1]= -wperp**2.*y*denom*Xfac/wmag2
out[:,2]= -(b*tilew[:,0]/wperp+y*wpar*tilew[:,2]/wmag2)*denom*Xfac
# deal w/ perpendicular impacts
wperp0Indx= numpy.fabs(wperp) < 10.**-10.
out[wperp0Indx,0]= (b-y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,0]/wmag2[wperp0Indx])*denom[wperp0Indx]*Xfac[wperp0Indx]
out[wperp0Indx,2]=-(b+y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,2]/wmag2[wperp0Indx])*denom[wperp0Indx]*Xfac[wperp0Indx]
# Rotate back to the original frame
return 2.0*GM*numpy.sum(\
rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1)
[docs]def impulse_deltav_hernquist_curvedstream(v,x,b,w,x0,v0,GM,rs):
"""
NAME:
impulse_deltav_plummer_hernquist
PURPOSE:
calculate the delta velocity to due an encounter with a Hernquist sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
b - impact parameter
w - velocity of the Hernquist sphere (3)
x0 - point of closest approach
v0 - velocity of point of closest approach
GM - mass of the Hernquist sphere (in natural units)
rs - size of the Hernquist sphere
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-08-13 - SANDERS, using Wyn Evans calculation
"""
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
b_ = b0+x-x0
w = w-v
wmag = numpy.sqrt(numpy.sum(w**2,axis=1))
bdotw=numpy.sum(b_*w,axis=1)/wmag
B = numpy.sqrt(numpy.sum(b_**2,axis=1)-bdotw**2)
denom= wmag*(B**2-rs**2)
denom=1./denom
s = numpy.sqrt(2.*B/(rs+B))
HernquistXv=numpy.vectorize(HernquistX)
Xfac = 1.-2.*rs/(rs+B)*HernquistXv(s)
return -2.0*GM*((b_.T-bdotw*w.T/wmag)*Xfac*denom).T
def _a_integrand(T,y,b,w,pot,compt):
t = T/(1-T*T)
X = b+w*t+y*numpy.array([0,1,0])
r = numpy.sqrt(numpy.sum(X**2))
return (1+T*T)/(1-T*T)**2*evaluateRforces(pot,r,0.)*X[compt]/r
def _deltav_integrate(y,b,w,pot):
return numpy.array([integrate.quad(_a_integrand,-1.,1.,args=(y,b,w,pot,i))[0] for i in range(3)])
[docs]def impulse_deltav_general(v,y,b,w,pot):
"""
NAME:
impulse_deltav_general
PURPOSE:
calculate the delta velocity to due an encounter with a general spherical potential in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream
INPUT:
v - velocity of the stream (nstar,3)
y - position along the stream (nstar)
b - impact parameter
w - velocity of the subhalo (3)
pot - Potential object or list thereof (should be spherical)
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-05-04 - SANDERS
2015-06-15 - Tweak to use galpy' potential objects - Bovy (IAS)
"""
pot= flatten_potential(pot)
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
nv= v.shape[0]
# Build the rotation matrices and their inverse
rot= _rotation_vy(v)
rotinv= _rotation_vy(v,inv=True)
# Rotate the subhalo's velocity to the stream frames
tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1)
tilew[:,1]-=numpy.sqrt(numpy.sum(v**2.,axis=1))
wmag = numpy.sqrt(tilew[:,0]**2+tilew[:,2]**2)
b0 = b*numpy.array([-tilew[:,2]/wmag,numpy.zeros(nv),tilew[:,0]/wmag]).T
return numpy.array(list(map(lambda i:numpy.sum(i[3]
*_deltav_integrate(i[0],i[1],i[2],pot).T,axis=-1)
,zip(y,b0,tilew,rotinv))))
[docs]def impulse_deltav_general_curvedstream(v,x,b,w,x0,v0,pot):
"""
NAME:
impulse_deltav_general_curvedstream
PURPOSE:
calculate the delta velocity to due an encounter with a general spherical potential in the impulse approximation; allows for arbitrary velocity vectors and arbitrary shaped streams
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
b - impact parameter
w - velocity of the subhalo (3)
x0 - position of closest approach (3)
v0 - velocity of stream at closest approach (3)
pot - Potential object or list thereof (should be spherical)
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-05-04 - SANDERS
2015-06-15 - Tweak to use galpy' potential objects - Bovy (IAS)
"""
pot= flatten_potential(pot)
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
b_ = b0+x-x0
return numpy.array(list(map(lambda i:_deltav_integrate(0.,i[1],i[0],pot)
,zip(w-v,b_))))
[docs]def impulse_deltav_general_orbitintegration(v,x,b,w,x0,v0,pot,tmax,galpot,
tmaxfac=10.,nsamp=1000,
integrate_method='symplec4_c'):
"""
NAME:
impulse_deltav_general_orbitintegration
PURPOSE:
calculate the delta velocity to due an encounter with a general spherical potential NOT in the impulse approximation by integrating each particle in the underlying galactic potential; allows for arbitrary velocity vectors and arbitrary shaped streams.
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
b - impact parameter
w - velocity of the subhalo (3)
x0 - position of closest approach (3)
v0 - velocity of stream at closest approach (3)
pot - Potential object or list thereof (should be spherical)
tmax - maximum integration time
galpot - galpy Potential object or list thereof
nsamp(1000) - number of forward integration points
integrate_method= ('symplec4_c') orbit integrator to use (see Orbit.integrate)
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-08-17 - SANDERS
"""
galpot= flatten_potential(galpot)
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
nstar,ndim=numpy.shape(v)
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
times = numpy.linspace(0.,tmax,nsamp)
xres = numpy.zeros(shape=(len(x),nsamp*2-1,3))
R, phi, z= coords.rect_to_cyl(x[:,0],x[:,1],x[:,2])
vR, vp, vz= coords.rect_to_cyl_vec(v[:,0],v[:,1],v[:,2],
R,phi,z,cyl=True)
for i in range(nstar):
o = Orbit([R[i],vR[i],vp[i],z[i],vz[i],phi[i]])
o.integrate(times,galpot,method=integrate_method)
xres[i,nsamp:,0]=o.x(times)[1:]
xres[i,nsamp:,1]=o.y(times)[1:]
xres[i,nsamp:,2]=o.z(times)[1:]
oreverse = o.flip()
oreverse.integrate(times,galpot,method=integrate_method)
xres[i,:nsamp,0]=oreverse.x(times)[::-1]
xres[i,:nsamp,1]=oreverse.y(times)[::-1]
xres[i,:nsamp,2]=oreverse.z(times)[::-1]
times = numpy.concatenate((-times[::-1],times[1:]))
nsamp = len(times)
X = b0+xres-x0-numpy.outer(times,w)
r = numpy.sqrt(numpy.sum(X**2,axis=-1))
acc = (numpy.reshape(evaluateRforces(pot,r.flatten(),0.),(nstar,nsamp))/r)[:,:,numpy.newaxis]*X
return integrate.simps(acc,x=times,axis=1)
[docs]def impulse_deltav_general_fullplummerintegration(v,x,b,w,x0,v0,galpot,GM,rs,
tmaxfac=10.,N=1000,
integrate_method='symplec4_c'):
"""
NAME:
impulse_deltav_general_fullplummerintegration
PURPOSE:
calculate the delta velocity to due an encounter with a moving Plummer sphere and galactic potential relative to just in galactic potential
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
b - impact parameter
w - velocity of the subhalo (3)
x0 - position of closest approach (3)
v0 - velocity of stream at closest approach (3)
galpot - Galaxy Potential object
GM - mass of Plummer
rs - scale of Plummer
tmaxfac(10) - multiple of rs/fabs(w - v0) to use for time integration interval
N(1000) - number of forward integration points
integrate_method('symplec4_c') - orbit integrator to use (see Orbit.integrate)
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-08-18 - SANDERS
"""
galpot= flatten_potential(galpot)
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
nstar,ndim=numpy.shape(v)
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
X = x0-b0
# Setup Plummer orbit
R, phi, z= coords.rect_to_cyl(X[0],X[1],X[2])
vR, vp, vz= coords.rect_to_cyl_vec(w[0],w[1],w[2],R,phi,z,cyl=True)
tmax = tmaxfac*rs/numpy.sqrt(numpy.sum((w-v0)**2))
times = numpy.linspace(0.,tmax,N)
dtimes = numpy.linspace(-tmax,tmax,2*N)
o = Orbit(vxvv=[R,-vR,-vp,z,-vz,phi])
o.integrate(times,galpot,method=integrate_method)
oplum = o(times[-1]).flip()
oplum.integrate(dtimes,galpot,method=integrate_method)
plumpot = MovingObjectPotential(orbit=oplum,
pot=PlummerPotential(amp=GM,b=rs))
# Now integrate each particle backwards in galaxy potential, forwards in combined potential and backwards again in galaxy and take diff
deltav = numpy.zeros((nstar,3))
R, phi, z= coords.rect_to_cyl(x[:,0],x[:,1],x[:,2])
vR, vp, vz= coords.rect_to_cyl_vec(v[:,0],v[:,1],v[:,2],
R,phi,z,cyl=True)
for i in range(nstar):
ostar= Orbit(vxvv=[R[i],-vR[i],-vp[i],z[i],-vz[i],phi[i]])
ostar.integrate(times,galpot,method=integrate_method)
oboth = ostar(times[-1]).flip()
oboth.integrate(dtimes,[galpot,plumpot],method=integrate_method)
ogalpot = oboth(times[-1]).flip()
ogalpot.integrate(times,galpot,method=integrate_method)
deltav[i][0] = -ogalpot.vx(times[-1]) - v[i][0]
deltav[i][1] = -ogalpot.vy(times[-1]) - v[i][1]
deltav[i][2] = -ogalpot.vz(times[-1]) - v[i][2]
return deltav
def _astream_integrand_x(t,y,v,b,w,b2,w2,wperp,wperp2,wpar,GSigma,rs2):
return GSigma(t)*(b*w2*w[2]/wperp-(y-v*t)*wpar*w[0])\
/((b2+rs2)*w2+wperp2*(y-v*t)**2.)
def _astream_integrand_y(t,y,v,b2,w2,wperp2,GSigma,rs2):
return GSigma(t)*(y-v*t)/((b2+rs2)*w2+wperp2*(y-v*t)**2.)
def _astream_integrand_z(t,y,v,b,w,b2,w2,wperp,wperp2,wpar,GSigma,rs2):
return -GSigma(t)*(b*w2*w[0]/wperp+(y-v*t)*wpar*w[2])\
/((b2+rs2)*w2+wperp2*(y-v*t)**2.)
def impulse_deltav_plummerstream(v,y,b,w,GSigma,rs,tmin=None,tmax=None):
"""
NAME:
impulse_deltav_plummerstream
PURPOSE:
calculate the delta velocity to due an encounter with a Plummer-softened stream in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream
INPUT:
v - velocity of the stream (nstar,3)
y - position along the stream (nstar)
b - impact parameter
w - velocity of the Plummer sphere (3)
GSigma - surface density of the Plummer-softened stream (in natural units); should be a function of time
rs - size of the Plummer sphere
tmin, tmax= (None) minimum and maximum time to consider for GSigma (need to be set)
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-11-14 - Written - Bovy (UofT)
"""
if len(v.shape) == 1:
v= numpy.reshape(v,(1,3))
y= numpy.reshape(y,(1,1))
if tmax is None or tmax is None:
raise ValueError("tmin= and tmax= need to be set")
nv= v.shape[0]
vmag= numpy.sqrt(numpy.sum(v**2.,axis=1))
# Build the rotation matrices and their inverse
rot= _rotation_vy(v)
rotinv= _rotation_vy(v,inv=True)
# Rotate the perturbing stream's velocity to the stream frames
tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1)
# Use similar expressions to Denis'
wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.)
wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1]
wmag2= wpar**2.+wperp**2.
wmag= numpy.sqrt(wmag2)
b2= b**2.
rs2= rs**2.
wperp2= wperp**2.
out= numpy.empty_like(v)
out[:,0]= [1./wmag[ii]\
*integrate.quad(_astream_integrand_x,
tmin,tmax,args=(y[ii],
vmag[ii],b,tilew[ii],
b2,wmag2[ii],
wperp[ii],wperp2[ii],
wpar[ii],
GSigma,rs2))[0]
for ii in range(len(y))]
out[:,1]= [-wperp2[ii]/wmag[ii]\
*integrate.quad(_astream_integrand_y,
tmin,tmax,args=(y[ii],
vmag[ii],
b2,wmag2[ii],
wperp2[ii],
GSigma,rs2))[0]
for ii in range(len(y))]
out[:,2]= [1./wmag[ii]\
*integrate.quad(_astream_integrand_z ,
tmin,tmax,args=(y[ii],
vmag[ii],b,tilew[ii],
b2,wmag2[ii],
wperp[ii],wperp2[ii],
wpar[ii],
GSigma,rs2))[0]
for ii in range(len(y))]
# Rotate back to the original frame
return 2.0*numpy.sum(\
rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1)
def _astream_integrand(t,b_,orb,tx,w,GSigma,rs2,tmin,compt):
teval= tx-tmin-t
b__= b_+numpy.array([orb.x(teval),orb.y(teval),orb.z(teval)])
w = w-numpy.array([orb.vx(teval),orb.vy(teval),orb.vz(teval)])
wmag = numpy.sqrt(numpy.sum(w**2))
bdotw=numpy.sum(b__*w)/wmag
denom= wmag*(numpy.sum(b__**2)+rs2-bdotw**2)
denom = 1./denom
return -2.0*GSigma(t)*(((b__.T-bdotw*w.T/wmag)*denom).T)[compt]
def _astream_integrate(b_,orb,tx,w,GSigma,rs2,otmin,tmin,tmax):
return numpy.array([integrate.quad(_astream_integrand,tmin,tmax,
args=(b_,orb,tx,w,GSigma,rs2,
otmin,i))[0] \
for i in range(3)])
def impulse_deltav_plummerstream_curvedstream(v,x,t,b,w,x0,v0,GSigma,rs,
galpot,tmin=None,tmax=None):
"""
NAME:
impulse_deltav_plummerstream_curvedstream
PURPOSE:
calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream; velocities and positions are assumed to lie along an orbit
INPUT:
v - velocity of the stream (nstar,3)
x - position along the stream (nstar,3)
t - times at which (v,x) are reached, wrt the closest impact t=0 (nstar)
b - impact parameter
w - velocity of the Plummer sphere (3)
x0 - point of closest approach
v0 - velocity of point of closest approach
GSigma - surface density of the Plummer-softened stream (in natural units); should be a function of time
rs - size of the Plummer sphere
galpot - galpy Potential object or list thereof
tmin, tmax= (None) minimum and maximum time to consider for GSigma
OUTPUT:
deltav (nstar,3)
HISTORY:
2015-11-20 - Written based on Plummer sphere above - Bovy (UofT)
"""
galpot= flatten_potential(galpot)
if len(v.shape) == 1: v= numpy.reshape(v,(1,3))
if len(x.shape) == 1: x= numpy.reshape(x,(1,3))
# Integrate an orbit to use to figure out where each (v,x) is at each time
R, phi, z= coords.rect_to_cyl(x0[0],x0[1],x0[2])
vR, vT, vz= coords.rect_to_cyl_vec(v0[0],v0[1],v0[2],R,phi,z,cyl=True)
# First back, then forward to cover the entire range with 1 orbit
o= Orbit([R,vR,vT,z,vz,phi]).flip()
ts= numpy.linspace(0.,numpy.fabs(numpy.amin(t)+tmin),101)
o.integrate(ts,galpot)
o= o(ts[-1]).flip()
ts= numpy.linspace(0.,numpy.amax(t)+tmax-numpy.amin(t)-tmin,201)
o.integrate(ts,galpot)
# Calculate kicks
b0 = numpy.cross(w,v0)
b0 *= b/numpy.sqrt(numpy.sum(b0**2))
return numpy.array(list(map(lambda i:_astream_integrate(\
b0-x0,o,i,w,GSigma,rs**2.,numpy.amin(t)+tmin,
tmin,tmax),t)))
def _rotation_vy(v,inv=False):
return _rotate_to_arbitrary_vector(v,[0,1,0],inv)