###############################################################################
# actionAngle: a Python module to calculate actions, angles, and frequencies
#
# class: actionAngleStaeckel
#
# Use Binney (2012; MNRAS 426, 1324)'s Staeckel approximation for
# calculating the actions
#
# methods:
# __call__: returns (jr,lz,jz)
#
###############################################################################
import copy
import warnings
import numpy
from scipy import optimize, integrate
from ..potential import evaluateR2derivs, evaluatez2derivs, \
evaluateRzderivs, epifreq, omegac, verticalfreq, MWPotential
from ..potential.Potential import _evaluatePotentials, \
_evaluateRforces, _evaluatezforces
from ..potential.Potential import flatten as flatten_potential
from ..util import coords #for prolate confocal transforms
from ..util import galpyWarning, conversion
from ..util.conversion import physical_conversion, \
potential_physical_input
from .actionAngle import actionAngle, UnboundError
from . import actionAngleStaeckel_c
from .actionAngleStaeckel_c import _ext_loaded as ext_loaded
from ..potential.Potential import _check_c
[docs]class actionAngleStaeckel(actionAngle):
"""Action-angle formalism for axisymmetric potentials using Binney (2012)'s Staeckel approximation"""
[docs] def __init__(self,*args,**kwargs):
"""
NAME:
__init__
PURPOSE:
initialize an actionAngleStaeckel object
INPUT:
pot= potential or list of potentials (3D)
delta= focus (can be Quantity)
useu0 - use u0 to calculate dV (NOT recommended)
c= if True, always use C for calculations
order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action, frequency, and angle integrals
ro= distance from vantage point to GC (kpc; can be Quantity)
vo= circular velocity at ro (km/s; can be Quantity)
OUTPUT:
instance
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
actionAngle.__init__(self,
ro=kwargs.get('ro',None),vo=kwargs.get('vo',None))
if not 'pot' in kwargs: #pragma: no cover
raise IOError("Must specify pot= for actionAngleStaeckel")
self._pot= flatten_potential(kwargs['pot'])
if self._pot == MWPotential:
warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy",
galpyWarning)
if not 'delta' in kwargs: #pragma: no cover
raise IOError("Must specify delta= for actionAngleStaeckel")
if ext_loaded and (('c' in kwargs and kwargs['c'])
or not 'c' in kwargs):
self._c= _check_c(self._pot)
if 'c' in kwargs and kwargs['c'] and not self._c:
warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) #pragma: no cover
else:
self._c= False
self._useu0= kwargs.get('useu0',False)
self._delta= kwargs['delta']
self._order= kwargs.get('order',10)
self._delta= conversion.parse_length(self._delta,ro=self._ro)
# Check the units
self._check_consistent_units()
return None
def _evaluate(self,*args,**kwargs):
"""
NAME:
__call__ (_evaluate)
PURPOSE:
evaluate the actions (jr,lz,jz)
INPUT:
Either:
a) R,vR,vT,z,vz[,phi]:
1) floats: phase-space value for single object (phi is optional) (each can be a Quantity)
2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity)
b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument
delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points
u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed)
c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation
order= (object-wide default, int) number of points to use in the Gauss-Legendre numerical integration of the relevant action integrals
When not using C:
fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad)
scipy.integrate.fixed_quad or .quad keywords
OUTPUT:
(jr,lz,jz)
HISTORY:
2012-11-27 - Written - Bovy (IAS)
2017-12-27 - Allowed individual delta for each point - Bovy (UofT)
"""
delta= kwargs.pop('delta',self._delta)
order= kwargs.get('order',self._order)
if len(args) == 5: #R,vR.vT, z, vz
R,vR,vT, z, vz= args
elif len(args) == 6: #R,vR.vT, z, vz, phi
R,vR,vT, z, vz, phi= args
else:
self._parse_eval_args(*args)
R= self._eval_R
vR= self._eval_vR
vT= self._eval_vT
z= self._eval_z
vz= self._eval_vz
if isinstance(R,float):
R= numpy.array([R])
vR= numpy.array([vR])
vT= numpy.array([vT])
z= numpy.array([z])
vz= numpy.array([vz])
if ((self._c and not ('c' in kwargs and not kwargs['c']))\
or (ext_loaded and (('c' in kwargs and kwargs['c'])))) \
and _check_c(self._pot):
Lz= R*vT
if self._useu0:
#First calculate u0
if 'u0' in kwargs:
u0= numpy.asarray(kwargs['u0'])
else:
E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii])
+vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))])
u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\
E,Lz,self._pot,delta)[0]
kwargs.pop('u0',None)
else:
u0= None
jr, jz, err= actionAngleStaeckel_c.actionAngleStaeckel_c(\
self._pot,delta,R,vR,vT,z,vz,u0=u0,order=order)
if err == 0:
return (jr,Lz,jz)
else: #pragma: no cover
raise RuntimeError("C-code for calculation actions failed; try with c=False")
else:
if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover
warnings.warn("C module not used because potential does not have a C implementation",galpyWarning)
kwargs.pop('c',None)
if len(R) > 1:
ojr= numpy.zeros((len(R)))
olz= numpy.zeros((len(R)))
ojz= numpy.zeros((len(R)))
for ii in range(len(R)):
targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii])
tkwargs= copy.copy(kwargs)
try:
tkwargs['delta']= delta[ii]
except (TypeError,IndexError):
tkwargs['delta']= delta
tjr,tlz,tjz= self(*targs,**tkwargs)
ojr[ii]= tjr
ojz[ii]= tjz
olz[ii]= tlz
return (ojr,olz,ojz)
else:
#Set up the actionAngleStaeckelSingle object
aASingle= actionAngleStaeckelSingle(R[0],vR[0],vT[0],
z[0],vz[0],pot=self._pot,
delta=delta)
return (numpy.atleast_1d(aASingle.JR(**copy.copy(kwargs))),
numpy.atleast_1d(aASingle._R*aASingle._vT),
numpy.atleast_1d(aASingle.Jz(**copy.copy(kwargs))))
def _actionsFreqs(self,*args,**kwargs):
"""
NAME:
actionsFreqs (_actionsFreqs)
PURPOSE:
evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz)
INPUT:
Either:
a) R,vR,vT,z,vz[,phi]:
1) floats: phase-space value for single object (phi is optional) (each can be a Quantity)
2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity)
b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument
delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points
u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed)
c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation
order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action and frequency integrals
When not using C:
fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad)
scipy.integrate.fixed_quad or .quad keywords
OUTPUT:
(jr,lz,jz,Omegar,Omegaphi,Omegaz)
HISTORY:
2013-08-28 - Written - Bovy (IAS)
"""
delta= kwargs.pop('delta',self._delta)
order= kwargs.get('order',self._order)
if ((self._c and not ('c' in kwargs and not kwargs['c']))\
or (ext_loaded and (('c' in kwargs and kwargs['c'])))) \
and _check_c(self._pot):
if len(args) == 5: #R,vR.vT, z, vz
R,vR,vT, z, vz= args
elif len(args) == 6: #R,vR.vT, z, vz, phi
R,vR,vT, z, vz, phi= args
else:
self._parse_eval_args(*args)
R= self._eval_R
vR= self._eval_vR
vT= self._eval_vT
z= self._eval_z
vz= self._eval_vz
if isinstance(R,float):
R= numpy.array([R])
vR= numpy.array([vR])
vT= numpy.array([vT])
z= numpy.array([z])
vz= numpy.array([vz])
Lz= R*vT
if self._useu0:
#First calculate u0
if 'u0' in kwargs:
u0= numpy.asarray(kwargs['u0'])
else:
E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii])
+vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))])
u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\
E,Lz,self._pot,delta)[0]
kwargs.pop('u0',None)
else:
u0= None
jr, jz, Omegar, Omegaphi, Omegaz, err= actionAngleStaeckel_c.actionAngleFreqStaeckel_c(\
self._pot,delta,R,vR,vT,z,vz,u0=u0,order=order)
# Adjustements for close-to-circular orbits
indx= numpy.isnan(Omegar)*(jr < 10.**-3.)+numpy.isnan(Omegaz)*(jz < 10.**-3.) #Close-to-circular and close-to-the-plane orbits
if numpy.sum(indx) > 0:
Omegar[indx]= [epifreq(self._pot,r,use_physical=False) for r in R[indx]]
Omegaphi[indx]= [omegac(self._pot,r,use_physical=False) for r in R[indx]]
Omegaz[indx]= [verticalfreq(self._pot,r,use_physical=False) for r in R[indx]]
if err == 0:
return (jr,Lz,jz,Omegar,Omegaphi,Omegaz)
else: #pragma: no cover
raise RuntimeError("C-code for calculation actions failed; try with c=False")
else:
if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover
warnings.warn("C module not used because potential does not have a C implementation",galpyWarning)
raise NotImplementedError("actionsFreqs with c=False not implemented")
def _actionsFreqsAngles(self,*args,**kwargs):
"""
NAME:
actionsFreqsAngles (_actionsFreqsAngles)
PURPOSE:
evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez)
INPUT:
Either:
a) R,vR,vT,z,vz[,phi]:
1) floats: phase-space value for single object (phi is optional) (each can be a Quantity)
2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity)
b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument
delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points
u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed)
c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation
order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action, frequency, and angle integrals
When not using C:
fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad)
scipy.integrate.fixed_quad or .quad keywords
OUTPUT:
(jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez)
HISTORY:
2013-08-28 - Written - Bovy (IAS)
"""
delta= kwargs.pop('delta',self._delta)
order= kwargs.get('order',self._order)
if ((self._c and not ('c' in kwargs and not kwargs['c']))\
or (ext_loaded and (('c' in kwargs and kwargs['c'])))) \
and _check_c(self._pot):
if len(args) == 5: #R,vR.vT, z, vz pragma: no cover
raise IOError("Must specify phi")
elif len(args) == 6: #R,vR.vT, z, vz, phi
R,vR,vT, z, vz, phi= args
else:
self._parse_eval_args(*args)
R= self._eval_R
vR= self._eval_vR
vT= self._eval_vT
z= self._eval_z
vz= self._eval_vz
phi= self._eval_phi
if isinstance(R,float):
R= numpy.array([R])
vR= numpy.array([vR])
vT= numpy.array([vT])
z= numpy.array([z])
vz= numpy.array([vz])
phi= numpy.array([phi])
Lz= R*vT
if self._useu0:
#First calculate u0
if 'u0' in kwargs:
u0= numpy.asarray(kwargs['u0'])
else:
E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii])
+vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))])
u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\
E,Lz,self._pot,delta)[0]
kwargs.pop('u0',None)
else:
u0= None
jr, jz, Omegar, Omegaphi, Omegaz, angler, anglephi,anglez, err= actionAngleStaeckel_c.actionAngleFreqAngleStaeckel_c(\
self._pot,delta,R,vR,vT,z,vz,phi,u0=u0,order=order)
# Adjustements for close-to-circular orbits
indx= numpy.isnan(Omegar)*(jr < 10.**-3.)+numpy.isnan(Omegaz)*(jz < 10.**-3.) #Close-to-circular and close-to-the-plane orbits
if numpy.sum(indx) > 0:
Omegar[indx]= [epifreq(self._pot,r,use_physical=False) for r in R[indx]]
Omegaphi[indx]= [omegac(self._pot,r,use_physical=False) for r in R[indx]]
Omegaz[indx]= [verticalfreq(self._pot,r,use_physical=False) for r in R[indx]]
if err == 0:
return (jr,Lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez)
else:
raise RuntimeError("C-code for calculation actions failed; try with c=False") #pragma: no cover
else: #pragma: no cover
if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover
warnings.warn("C module not used because potential does not have a C implementation",galpyWarning)
raise NotImplementedError("actionsFreqs with c=False not implemented")
def _EccZmaxRperiRap(self,*args,**kwargs):
"""
NAME:
EccZmaxRperiRap (_EccZmaxRperiRap)
PURPOSE:
evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the Staeckel approximation
INPUT:
Either:
a) R,vR,vT,z,vz[,phi]:
1) floats: phase-space value for single object (phi is optional) (each can be a Quantity)
2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity)
b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument
delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points
u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed)
c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation
OUTPUT:
(e,zmax,rperi,rap)
HISTORY:
2017-12-12 - Written - Bovy (UofT)
"""
delta= kwargs.get('delta',self._delta)
umin, umax, vmin= self._uminumaxvmin(*args,**kwargs)
rperi= coords.uv_to_Rz(umin,numpy.pi/2.,delta=delta)[0]
rap_tmp, zmax= coords.uv_to_Rz(umax,vmin,delta=delta)
rap= numpy.sqrt(rap_tmp**2.+zmax**2.)
e= (rap-rperi)/(rap+rperi)
return (e,zmax,rperi,rap)
def _uminumaxvmin(self,*args,**kwargs):
"""
NAME:
_uminumaxvmin
PURPOSE:
evaluate u_min, u_max, and v_min
INPUT:
Either:
a) R,vR,vT,z,vz
b) Orbit instance: initial condition used if that's it, orbit(t)
if there is a time given as well
c= True/False; overrides the object's c= keyword to use C or not
OUTPUT:
(umin,umax,vmin)
HISTORY:
2017-12-12 - Written - Bovy (UofT)
"""
delta= kwargs.pop('delta',self._delta)
if len(args) == 5: #R,vR.vT, z, vz
R,vR,vT, z, vz= args
elif len(args) == 6: #R,vR.vT, z, vz, phi
R,vR,vT, z, vz, phi= args
else:
self._parse_eval_args(*args)
R= self._eval_R
vR= self._eval_vR
vT= self._eval_vT
z= self._eval_z
vz= self._eval_vz
if isinstance(R,float):
R= numpy.array([R])
vR= numpy.array([vR])
vT= numpy.array([vT])
z= numpy.array([z])
vz= numpy.array([vz])
if ((self._c and not ('c' in kwargs and not kwargs['c']))\
or (ext_loaded and (('c' in kwargs and kwargs['c'])))) \
and _check_c(self._pot):
Lz= R*vT
if self._useu0:
#First calculate u0
if 'u0' in kwargs:
u0= numpy.asarray(kwargs['u0'])
else:
E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii])
+vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))])
u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\
E,Lz,self._pot,delta)[0]
kwargs.pop('u0',None)
else:
u0= None
umin, umax, vmin, err= \
actionAngleStaeckel_c.actionAngleUminUmaxVminStaeckel_c(\
self._pot,delta,R,vR,vT,z,vz,u0=u0)
if err == 0:
return (umin,umax,vmin)
else: #pragma: no cover
raise RuntimeError("C-code for calculation actions failed; try with c=False")
else:
if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover
warnings.warn("C module not used because potential does not have a C implementation",galpyWarning)
kwargs.pop('c',None)
if len(R) > 1:
oumin= numpy.zeros((len(R)))
oumax= numpy.zeros((len(R)))
ovmin= numpy.zeros((len(R)))
for ii in range(len(R)):
targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii])
tkwargs= copy.copy(kwargs)
try:
tkwargs['delta']= delta[ii]
except TypeError:
tkwargs['delta']= delta
tumin,tumax,tvmin= self._uminumaxvmin(*targs,**tkwargs)
oumin[ii]= tumin
oumax[ii]= tumax
ovmin[ii]= tvmin
return (oumin,oumax,ovmin)
else:
#Set up the actionAngleStaeckelSingle object
aASingle= actionAngleStaeckelSingle(R[0],vR[0],vT[0],
z[0],vz[0],pot=self._pot,
delta=delta)
umin, umax= aASingle.calcUminUmax()
vmin= aASingle.calcVmin()
return (numpy.atleast_1d(umin),
numpy.atleast_1d(umax),
numpy.atleast_1d(vmin))
class actionAngleStaeckelSingle(actionAngle):
"""Action-angle formalism for axisymmetric potentials using Binney (2012)'s Staeckel approximation"""
def __init__(self,*args,**kwargs):
"""
NAME:
__init__
PURPOSE:
initialize an actionAngleStaeckelSingle object
INPUT:
Either:
a) R,vR,vT,z,vz
b) Orbit instance: initial condition used if that's it, orbit(t)
if there is a time given as well
pot= potential or list of potentials
OUTPUT:
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
self._parse_eval_args(*args,_noOrbUnitsCheck=True,**kwargs)
self._R= self._eval_R
self._vR= self._eval_vR
self._vT= self._eval_vT
self._z= self._eval_z
self._vz= self._eval_vz
if not 'pot' in kwargs: #pragma: no cover
raise IOError("Must specify pot= for actionAngleStaeckelSingle")
self._pot= kwargs['pot']
if not 'delta' in kwargs: #pragma: no cover
raise IOError("Must specify delta= for actionAngleStaeckel")
self._delta= kwargs['delta']
#Pre-calculate everything
self._ux, self._vx= coords.Rz_to_uv(self._R,self._z,
delta=self._delta)
self._sinvx= numpy.sin(self._vx)
self._cosvx= numpy.cos(self._vx)
self._coshux= numpy.cosh(self._ux)
self._sinhux= numpy.sinh(self._ux)
self._pux= self._delta*(self._vR*self._coshux*self._sinvx
+self._vz*self._sinhux*self._cosvx)
self._pvx= self._delta*(self._vR*self._sinhux*self._cosvx
-self._vz*self._coshux*self._sinvx)
EL= self.calcEL()
self._E= EL[0]
self._Lz= EL[1]
#Determine umin and umax
self._u0= kwargs.pop('u0',self._ux) #u0 as defined by Binney does not matter for a
#single action evaluation, so we don't determine it here
self._sinhu0= numpy.sinh(self._u0)
self._potu0v0= potentialStaeckel(self._u0,self._vx,
self._pot,self._delta)
self._I3U= self._E*self._sinhux**2.-self._pux**2./2./self._delta**2.\
-self._Lz**2./2./self._delta**2./self._sinhux**2.
self._potupi2= potentialStaeckel(self._ux,numpy.pi/2.,
self._pot,self._delta)
dV= (self._coshux**2.*self._potupi2
-(self._sinhux**2.+self._sinvx**2.)
*potentialStaeckel(self._ux,self._vx,
self._pot,self._delta))
self._I3V= -self._E*self._sinvx**2.+self._pvx**2./2./self._delta**2.\
+self._Lz**2./2./self._delta**2./self._sinvx**2.\
-dV
self.calcUminUmax()
self.calcVmin()
return None
def angleR(self,**kwargs):
"""
NAME:
AngleR
PURPOSE:
Calculate the radial angle
INPUT:
scipy.integrate.quadrature keywords
OUTPUT:
w_R(R,vT,vT) in radians +
estimate of the error (does not include TR error)
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
raise NotImplementedError("'angleR' not yet implemented for Staeckel approximation")
def TR(self,**kwargs):
"""
NAME:
TR
PURPOSE:
Calculate the radial period for a power-law rotation curve
INPUT:
scipy.integrate.quadrature keywords
OUTPUT:
T_R(R,vT,vT)*vc/ro + estimate of the error
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
raise NotImplementedError("'TR' not implemented yet for Staeckel approximation")
def Tphi(self,**kwargs):
"""
NAME:
Tphi
PURPOSE:
Calculate the azimuthal period
INPUT:
+scipy.integrate.quadrature keywords
OUTPUT:
T_phi(R,vT,vT)/ro/vc + estimate of the error
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
raise NotImplementedError("'Tphi' not implemented yet for Staeckel approxximation")
def I(self,**kwargs):
"""
NAME:
I
PURPOSE:
Calculate I, the 'ratio' between the radial and azimutha period
INPUT:
+scipy.integrate.quadrature keywords
OUTPUT:
I(R,vT,vT) + estimate of the error
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
raise NotImplementedError("'I' not implemented yet for Staeckel approxximation")
def Jphi(self): #pragma: no cover
"""
NAME:
Jphi
PURPOSE:
Calculate the azimuthal action
INPUT:
OUTPUT:
J_R(R,vT,vT)/ro/vc
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
return self._R*self._vT
def JR(self,**kwargs):
"""
NAME:
JR
PURPOSE:
Calculate the radial action
INPUT:
fixed_quad= (False) if True, use n=10 fixed_quad
+scipy.integrate.quad keywords
OUTPUT:
J_R(R,vT,vT)/ro/vc + estimate of the error (nan for fixed_quad)
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
if hasattr(self,'_JR'): #pragma: no cover
return self._JR
umin, umax= self.calcUminUmax()
#print self._ux, self._pux, (umax-umin)/umax
if (umax-umin)/umax < 10.**-6: return numpy.array([0.])
order= kwargs.pop('order',10)
if kwargs.pop('fixed_quad',False):
# factor in next line bc integrand=/2delta^2
self._JR= 1./numpy.pi*numpy.sqrt(2.)*self._delta\
*integrate.fixed_quad(_JRStaeckelIntegrand,
umin,umax,
args=(self._E,self._Lz,self._I3U,
self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
n=order,
**kwargs)[0]
else:
self._JR= 1./numpy.pi*numpy.sqrt(2.)*self._delta\
*integrate.quad(_JRStaeckelIntegrand,
umin,umax,
args=(self._E,self._Lz,self._I3U,
self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
**kwargs)[0]
return self._JR
def Jz(self,**kwargs):
"""
NAME:
Jz
PURPOSE:
Calculate the vertical action
INPUT:
fixed_quad= (False) if True, use n=10 fixed_quad
+scipy.integrate.quad keywords
OUTPUT:
J_z(R,vT,vT)/ro/vc + estimate of the error
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
if hasattr(self,'_JZ'): #pragma: no cover
return self._JZ
vmin= self.calcVmin()
if (numpy.pi/2.-vmin) < 10.**-7: return numpy.array([0.])
order= kwargs.pop('order',10)
if kwargs.pop('fixed_quad',False):
# factor in next line bc integrand=/2delta^2
self._JZ= 2./numpy.pi*numpy.sqrt(2.)*self._delta \
*integrate.fixed_quad(_JzStaeckelIntegrand,
vmin,numpy.pi/2,
args=(self._E,self._Lz,self._I3V,
self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot),
n=order,
**kwargs)[0]
else:
# factor in next line bc integrand=/2delta^2
self._JZ= 2./numpy.pi*numpy.sqrt(2.)*self._delta \
*integrate.quad(_JzStaeckelIntegrand,
vmin,numpy.pi/2,
args=(self._E,self._Lz,self._I3V,
self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot),
**kwargs)[0]
return self._JZ
def calcEL(self,**kwargs):
"""
NAME:
calcEL
PURPOSE:
calculate the energy and angular momentum
INPUT:
scipy.integrate.quadrature keywords
OUTPUT:
(E,L)
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
E,L= calcELStaeckel(self._R,self._vR,self._vT,self._z,self._vz,
self._pot)
return (E,L)
def calcUminUmax(self,**kwargs):
"""
NAME:
calcUminUmax
PURPOSE:
calculate the u 'apocenter' and 'pericenter'
INPUT:
OUTPUT:
(umin,umax)
HISTORY:
2012-11-27 - Written - Bovy (IAS)
"""
if hasattr(self,'_uminumax'): #pragma: no cover
return self._uminumax
E, L= self._E, self._Lz
# Calculate value of the integrand at current point, to check whether
# we are at a turning point
current_val= _JRStaeckelIntegrandSquared(self._ux,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot)
if numpy.fabs(self._pux) < 1e-7 \
or numpy.fabs(current_val) < 1e-10: #We are at umin or umax
eps= 10.**-8.
peps= _JRStaeckelIntegrandSquared(self._ux+eps,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot)
meps= _JRStaeckelIntegrandSquared(self._ux-eps,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot)
if peps < 0. and meps > 0.: #we are at umax
umax= self._ux
rstart,prevr= _uminUmaxFindStart(self._ux,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot)
if rstart == 0.: umin= 0.
else:
try:
umin= optimize.brentq(_JRStaeckelIntegrandSquared,
rstart,self._ux-eps,
(E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
maxiter=200)
except RuntimeError: #pragma: no cover
raise UnboundError("Orbit seems to be unbound")
elif peps > 0. and meps < 0.: #we are at umin
umin= self._ux
rend,prevr= _uminUmaxFindStart(self._ux,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot,
umax=True)
umax= optimize.brentq(_JRStaeckelIntegrandSquared,
self._ux+eps,rend,
(E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
maxiter=200)
else: #circular orbit
umin= self._ux
umax= self._ux
else:
rstart,prevr= _uminUmaxFindStart(self._ux,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot)
if rstart == 0.: umin= 0.
else:
if numpy.fabs(prevr-self._ux) < 10.**-2.: rup= self._ux
else: rup= prevr
try:
umin= optimize.brentq(_JRStaeckelIntegrandSquared,
rstart,rup,
(E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
maxiter=200)
except RuntimeError: #pragma: no cover
raise UnboundError("Orbit seems to be unbound")
rend,prevr= _uminUmaxFindStart(self._ux,
E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot,
umax=True)
umax= optimize.brentq(_JRStaeckelIntegrandSquared,
prevr,rend,
(E,L,self._I3U,self._delta,
self._u0,self._sinhu0**2.,
self._vx,self._sinvx**2.,
self._potu0v0,self._pot),
maxiter=200)
self._uminumax= (umin,umax)
return self._uminumax
def calcVmin(self,**kwargs):
"""
NAME:
calcVmin
PURPOSE:
calculate the v 'pericenter'
INPUT:
OUTPUT:
vmin
HISTORY:
2012-11-28 - Written - Bovy (IAS)
"""
if hasattr(self,'_vmin'): #pragma: no cover
return self._vmin
E, L= self._E, self._Lz
if numpy.fabs(self._pvx) < 10.**-7.: #We are at vmin or vmax
eps= 10.**-8.
peps= _JzStaeckelIntegrandSquared(self._vx+eps,
E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot)
meps= _JzStaeckelIntegrandSquared(self._vx-eps,
E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot)
if peps < 0. and meps > 0.: #we are at vmax, which cannot happen
rstart= _vminFindStart(self._vx,
E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot)
if rstart == 0.: vmin= 0.
else:
try:
vmin= optimize.brentq(_JzStaeckelIntegrandSquared,
rstart,self._vx-eps,
(E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot),
maxiter=200)
except RuntimeError: #pragma: no cover
raise UnboundError("Orbit seems to be unbound")
elif peps > 0. and meps < 0.: #we are at vmin
vmin= self._vx
else: #planar orbit
vmin= self._vx
else:
rstart= _vminFindStart(self._vx,
E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot)
if rstart == 0.: vmin= 0.
else:
try:
vmin= optimize.brentq(_JzStaeckelIntegrandSquared,
rstart,rstart/0.9,
(E,L,self._I3V,self._delta,
self._ux,self._coshux**2.,
self._sinhux**2.,
self._potupi2,self._pot),
maxiter=200)
except RuntimeError: #pragma: no cover
raise UnboundError("Orbit seems to be unbound")
self._vmin= vmin
return self._vmin
def calcELStaeckel(R,vR,vT,z,vz,pot,vc=1.,ro=1.):
"""
NAME:
calcELStaeckel
PURPOSE:
calculate the energy and angular momentum
INPUT:
R - Galactocentric radius (/ro)
vR - radial part of the velocity (/vc)
vT - azimuthal part of the velocity (/vc)
vc - circular velocity
ro - reference radius
OUTPUT:
(E,L)
HISTORY:
2012-11-30 - Written - Bovy (IAS)
"""
return (_evaluatePotentials(pot,R,z)+vR**2./2.+vT**2./2.+vz**2./2.,R*vT)
def potentialStaeckel(u,v,pot,delta):
"""
NAME:
potentialStaeckel
PURPOSE:
return the potential
INPUT:
u - confocal u
v - confocal v
pot - potential
delta - focus
OUTPUT:
Phi(u,v)
HISTORY:
2012-11-29 - Written - Bovy (IAS)
"""
R,z= coords.uv_to_Rz(u,v,delta=delta)
return _evaluatePotentials(pot,R,z)
def FRStaeckel(u,v,pot,delta): #pragma: no cover because unused
"""
NAME:
FRStaeckel
PURPOSE:
return the radial force
INPUT:
u - confocal u
v - confocal v
pot - potential
delta - focus
OUTPUT:
FR(u,v)
HISTORY:
2012-11-30 - Written - Bovy (IAS)
"""
R,z= coords.uv_to_Rz(u,v,delta=delta)
return _evaluateRforces(pot,R,z)
def FZStaeckel(u,v,pot,delta): #pragma: no cover because unused
"""
NAME:
FZStaeckel
PURPOSE:
return the vertical force
INPUT:
u - confocal u
v - confocal v
pot - potential
delta - focus
OUTPUT:
FZ(u,v)
HISTORY:
2012-11-30 - Written - Bovy (IAS)
"""
R,z= coords.uv_to_Rz(u,v,delta=delta)
return _evaluatezforces(pot,R,z)
def _JRStaeckelIntegrand(u,E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0,
potu0v0,pot):
return numpy.sqrt(_JRStaeckelIntegrandSquared(u,E,Lz,I3U,delta,u0,sinh2u0,
v0,sin2v0,
potu0v0,pot))
def _JRStaeckelIntegrandSquared(u,E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0,
potu0v0,pot):
#potu0v0= potentialStaeckel(u0,v0,pot,delta)
"""The J_R integrand: p^2_u(u)/2/delta^2"""
sinh2u= numpy.sinh(u)**2.
dU= (sinh2u+sin2v0)*potentialStaeckel(u,v0,
pot,delta)\
-(sinh2u0+sin2v0)*potu0v0
return E*sinh2u-I3U-dU-Lz**2./2./delta**2./sinh2u
def _JzStaeckelIntegrand(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0,
potu0pi2,pot):
return numpy.sqrt(_JzStaeckelIntegrandSquared(v,E,Lz,I3V,delta,u0,cosh2u0,
sinh2u0,
potu0pi2,pot))
def _JzStaeckelIntegrandSquared(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0,
potu0pi2,pot):
#potu0pi2= potentialStaeckel(u0,numpy.pi/2.,pot,delta)
"""The J_z integrand: p_v(v)/2/delta^2"""
sin2v= numpy.sin(v)**2.
dV= cosh2u0*potu0pi2\
-(sinh2u0+sin2v)*potentialStaeckel(u0,v,pot,delta)
return E*sin2v+I3V+dV-Lz**2./2./delta**2./sin2v
def _uminUmaxFindStart(u,
E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0,
potu0v0,pot,umax=False):
"""
NAME:
_uminUmaxFindStart
PURPOSE:
Find adequate start or end points to solve for umin and umax
INPUT:
same as JRStaeckelIntegrandSquared
OUTPUT:
rstart or rend
HISTORY:
2012-11-30 - Written - Bovy (IAS)
"""
if umax:
utry= u*1.1
else:
utry= u*0.9
prevu= u
while _JRStaeckelIntegrandSquared(utry,
E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0,
potu0v0,pot) >= 0. \
and utry > 0.000000001:
prevu= utry
if umax:
if utry > 100.:
raise UnboundError("Orbit seems to be unbound")
utry*= 1.1
else:
utry*= 0.9
if utry < 0.000000001: return (0.,prevu)
return (utry,prevu)
def _vminFindStart(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0,
potu0pi2,pot):
"""
NAME:
_vminFindStart
PURPOSE:
Find adequate start point to solve for vmin
INPUT:
same as JzStaeckelIntegrandSquared
OUTPUT:
rstart
HISTORY:
2012-11-28 - Written - Bovy (IAS)
"""
vtry= 0.9*v
while _JzStaeckelIntegrandSquared(vtry,
E,Lz,I3V,delta,u0,cosh2u0,sinh2u0,
potu0pi2,pot) >= 0. \
and vtry > 0.000000001:
vtry*= 0.9
if vtry < 0.000000001: return 0.
return vtry
@potential_physical_input
@physical_conversion('position',pop=True)
def estimateDeltaStaeckel(pot,R,z, no_median=False):
"""
NAME:
estimateDeltaStaeckel
PURPOSE:
Estimate a good value for delta using eqn. (9) in Sanders (2012)
INPUT:
pot - Potential instance or list thereof
R,z- coordinates (if these are arrays, the median estimated delta is returned, i.e., if this is an orbit)
no_median - (False) if True, and input is array, return all calculated values of delta (useful for quickly
estimating delta for many phase space points)
OUTPUT:
delta
HISTORY:
2013-08-28 - Written - Bovy (IAS)
2016-02-20 - Changed input order to allow physical conversions - Bovy (UofT)
"""
if isinstance(R,numpy.ndarray):
delta2= numpy.array([(z[ii]**2.-R[ii]**2. #eqn. (9) has a sign error
+(3.*R[ii]*_evaluatezforces(pot,R[ii],z[ii])
-3.*z[ii]*_evaluateRforces(pot,R[ii],z[ii])
+R[ii]*z[ii]*(evaluateR2derivs(pot,R[ii],z[ii],
use_physical=False)
-evaluatez2derivs(pot,R[ii],z[ii],
use_physical=False)))/evaluateRzderivs(pot,R[ii],z[ii],use_physical=False)) for ii in range(len(R))])
indx= (delta2 < 0.)*(delta2 > -10.**-10.)
delta2[indx]= 0.
if not no_median:
delta2= numpy.median(delta2[True^numpy.isnan(delta2)])
else:
delta2= (z**2.-R**2. #eqn. (9) has a sign error
+(3.*R*_evaluatezforces(pot,R,z)
-3.*z*_evaluateRforces(pot,R,z)
+R*z*(evaluateR2derivs(pot,R,z,use_physical=False)
-evaluatez2derivs(pot,R,z,use_physical=False)))/evaluateRzderivs(pot,R,z,use_physical=False))
if delta2 < 0. and delta2 > -10.**-10.: delta2= 0.
return numpy.sqrt(delta2)