###############################################################################
# actionAngle: a Python module to calculate actions, angles, and frequencies
#
# class: actionAngleIsochroneApprox
#
# Calculate actions-angle coordinates for any potential by using
# an isochrone potential as an approximate potential and using
# a Fox & Binney (2013?) + torus machinery-like algorithm
# (angle-fit) (Bovy 2014)
#
# methods:
# __call__: returns (jr,lz,jz)
# actionsFreqs: returns (jr,lz,jz,Or,Op,Oz)
# actionsFreqsAngles: returns (jr,lz,jz,Or,Op,Oz,ar,ap,az)
#
###############################################################################
import warnings
import numpy
from numpy import linalg
from scipy import optimize
from ..potential import dvcircdR, vcirc, _isNonAxi
from ..potential.Potential import flatten as flatten_potential
from .actionAngleIsochrone import actionAngleIsochrone
from .actionAngle import actionAngle
from ..potential import IsochronePotential, MWPotential
from ..util import bovy_plot, galpyWarning
from ..util.bovy_conversion import physical_conversion, \
potential_physical_input, time_in_Gyr
_TWOPI= 2.*numpy.pi
_ANGLETOL= 0.02 #tolerance for deciding whether full angle range is covered
_APY_LOADED= True
try:
from astropy import units
except ImportError:
_APY_LOADED= False
[docs]class actionAngleIsochroneApprox(actionAngle):
"""Action-angle formalism using an isochrone potential as an approximate potential and using a Fox & Binney (2014?) like algorithm to calculate the actions using orbit integrations and a torus-machinery-like angle-fit to get the angles and frequencies (Bovy 2014)"""
[docs] def __init__(self,*args,**kwargs):
"""
NAME:
__init__
PURPOSE:
initialize an actionAngleIsochroneApprox object
INPUT:
Either:
b= scale parameter of the isochrone parameter (can be Quantity)
ip= instance of a IsochronePotential
aAI= instance of an actionAngleIsochrone
pot= potential to calculate action-angle variables for
tintJ= (default: 100) time to integrate orbits for to estimate actions (can be Quantity)
ntintJ= (default: 10000) number of time-integration points
integrate_method= (default: 'dopr54_c') integration method to use
dt= (None) orbit.integrate dt keyword (for fixed stepsize integration)
maxn= (default: 3) Default value for all methods when using a grid in vec(n) up to this n (zero-based)
ro= distance from vantage point to GC (kpc; can be Quantity)
vo= circular velocity at ro (km/s; can be Quantity)
OUTPUT:
instance
HISTORY:
2013-09-10 - 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 actionAngleIsochroneApprox")
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 'b' in kwargs and not 'ip' in kwargs \
and not 'aAI' in kwargs: #pragma: no cover
raise IOError("Must specify b=, ip=, or aAI= for actionAngleIsochroneApprox")
if 'aAI' in kwargs:
if not isinstance(kwargs['aAI'],actionAngleIsochrone): #pragma: no cover
raise IOError("'Provided aAI= does not appear to be an instance of an actionAngleIsochrone")
self._aAI= kwargs['aAI']
elif 'ip' in kwargs:
ip= kwargs['ip']
if not isinstance(ip,IsochronePotential): #pragma: no cover
raise IOError("'Provided ip= does not appear to be an instance of an IsochronePotential")
self._aAI= actionAngleIsochrone(ip=ip)
else:
if _APY_LOADED and isinstance(kwargs['b'],units.Quantity):
b= kwargs['b'].to(units.kpc).value/self._ro
else:
b= kwargs['b']
self._aAI= actionAngleIsochrone(ip=IsochronePotential(b=b,
normalize=1.))
self._tintJ= kwargs.get('tintJ',100.)
if _APY_LOADED and isinstance(self._tintJ,units.Quantity):
self._tintJ= self._tintJ.to(units.Gyr).value\
/time_in_Gyr(self._vo,self._ro)
self._ntintJ= kwargs.get('ntintJ',10000)
self._integrate_dt= kwargs.get('dt',None)
self._tsJ= numpy.linspace(0.,self._tintJ,self._ntintJ)
self._integrate_method= kwargs.get('integrate_method','dopr54_c')
self._maxn= kwargs.get('maxn',3)
self._c= False
ext_loaded= False
if ext_loaded and (('c' in kwargs and kwargs['c'])
or not 'c' in kwargs): #pragma: no cover
self._c= True
else:
self._c= False
# 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
cumul= if True, return the cumulative average actions (to look
at convergence)
OUTPUT:
(jr,lz,jz)
HISTORY:
2013-09-10 - Written - Bovy (IAS)
"""
R,vR,vT,z,vz,phi= self._parse_args(False,False,*args)
if self._c: #pragma: no cover
pass
else:
#Use self._aAI to calculate the actions and angles in the isochrone potential
acfs= self._aAI._actionsFreqsAngles(R.flatten(),
vR.flatten(),
vT.flatten(),
z.flatten(),
vz.flatten(),
phi.flatten())
jrI= numpy.reshape(acfs[0],R.shape)[:,:-1]
jzI= numpy.reshape(acfs[2],R.shape)[:,:-1]
anglerI= numpy.reshape(acfs[6],R.shape)
anglezI= numpy.reshape(acfs[8],R.shape)
if numpy.any((numpy.fabs(numpy.amax(anglerI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglerI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full radial angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
if numpy.any((numpy.fabs(numpy.amax(anglezI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglezI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full vertical angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1]
danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1]
if kwargs.get('cumul',False):
sumFunc= numpy.cumsum
else:
sumFunc= numpy.sum
jr= sumFunc(jrI*danglerI,axis=1)/sumFunc(danglerI,axis=1)
jz= sumFunc(jzI*danglezI,axis=1)/sumFunc(danglezI,axis=1)
if _isNonAxi(self._pot):
lzI= numpy.reshape(acfs[1],R.shape)[:,:-1]
anglephiI= numpy.reshape(acfs[7],R.shape)
danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1]
if numpy.any((numpy.fabs(numpy.amax(anglephiI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglephiI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full azimuthal angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
lz= sumFunc(lzI*danglephiI,axis=1)/sumFunc(danglephiI,axis=1)
else:
lz= R[:,0]*vT[:,0]
return (jr,lz,jz)
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
maxn= (default: object-wide default) Use a grid in vec(n) up to this n (zero-based)
ts= if set, the phase-space points correspond to these times (IF NOT SET, WE ASSUME THAT ts IS THAT THAT IS ASSOCIATED WITH THIS OBJECT)
_firstFlip= (False) if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object
OUTPUT:
(jr,lz,jz,Omegar,Omegaphi,Omegaz)
HISTORY:
2013-09-10 - Written - Bovy (IAS)
"""
acfs= self._actionsFreqsAngles(*args,**kwargs)
return (acfs[0],acfs[1],acfs[2],acfs[3],acfs[4],acfs[5])
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
maxn= (default: object-wide default) Use a grid in vec(n) up to this n (zero-based)
ts= if set, the phase-space points correspond to these times (IF NOT SET, WE ASSUME THAT ts IS THAT THAT IS ASSOCIATED WITH THIS OBJECT)
_firstFlip= (False) if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object
OUTPUT:
(jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez)
HISTORY:
2013-09-10 - Written - Bovy (IAS)
"""
from ..orbit import Orbit
_firstFlip= kwargs.get('_firstFlip',False)
#If the orbit was already integrated, set ts to the integration times
if isinstance(args[0],Orbit) and hasattr(args[0],'orbit') \
and not 'ts' in kwargs:
kwargs['ts']= args[0].t
elif (isinstance(args[0],list) and isinstance(args[0][0],Orbit)) \
and hasattr(args[0][0],'orbit') \
and not 'ts' in kwargs:
kwargs['ts']= args[0][0].t
R,vR,vT,z,vz,phi= self._parse_args(True,_firstFlip,*args)
if 'ts' in kwargs and not kwargs['ts'] is None:
ts= kwargs['ts']
if _APY_LOADED and isinstance(ts,units.Quantity):
ts= ts.to(units.Gyr).value\
/time_in_Gyr(self._vo,self._ro)
else:
ts= numpy.empty(R.shape[1])
ts[self._ntintJ-1:]= self._tsJ
ts[:self._ntintJ-1]= -self._tsJ[1:][::-1]
maxn= kwargs.get('maxn',self._maxn)
if self._c: #pragma: no cover
pass
else:
#Use self._aAI to calculate the actions and angles in the isochrone potential
if '_acfs' in kwargs: acfs= kwargs['_acfs']
else:
acfs= self._aAI._actionsFreqsAngles(R.flatten(),
vR.flatten(),
vT.flatten(),
z.flatten(),
vz.flatten(),
phi.flatten())
jrI= numpy.reshape(acfs[0],R.shape)[:,:-1]
jzI= numpy.reshape(acfs[2],R.shape)[:,:-1]
anglerI= numpy.reshape(acfs[6],R.shape)
anglezI= numpy.reshape(acfs[8],R.shape)
if numpy.any((numpy.fabs(numpy.amax(anglerI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglerI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full radial angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
if numpy.any((numpy.fabs(numpy.amax(anglezI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglezI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full vertical angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1]
danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1]
jr= numpy.sum(jrI*danglerI,axis=1)/numpy.sum(danglerI,axis=1)
jz= numpy.sum(jzI*danglezI,axis=1)/numpy.sum(danglezI,axis=1)
if _isNonAxi(self._pot): #pragma: no cover
lzI= numpy.reshape(acfs[1],R.shape)[:,:-1]
anglephiI= numpy.reshape(acfs[7],R.shape)
if numpy.any((numpy.fabs(numpy.amax(anglephiI,axis=1)-_TWOPI) > _ANGLETOL)\
*(numpy.fabs(numpy.amin(anglephiI,axis=1)) > _ANGLETOL)): #pragma: no cover
warnings.warn("Full azimuthal angle range not covered for at least one object; actions are likely not reliable",galpyWarning)
danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1]
lz= numpy.sum(lzI*danglephiI,axis=1)/numpy.sum(danglephiI,axis=1)
else:
lz= R[:,len(ts)//2]*vT[:,len(ts)//2]
#Now do an 'angle-fit'
angleRT= dePeriod(numpy.reshape(acfs[6],R.shape))
acfs7= numpy.reshape(acfs[7],R.shape)
negFreqIndx= numpy.median(acfs7-numpy.roll(acfs7,1,axis=1),axis=1) < 0. #anglephi is decreasing
anglephiT= numpy.empty(acfs7.shape)
anglephiT[negFreqIndx,:]= dePeriod(_TWOPI-acfs7[negFreqIndx,:])
negFreqPhi= numpy.zeros(R.shape[0],dtype='bool')
negFreqPhi[negFreqIndx]= True
anglephiT[True^negFreqIndx,:]= dePeriod(acfs7[True^negFreqIndx,:])
angleZT= dePeriod(numpy.reshape(acfs[8],R.shape))
#Write the angle-fit as Y=AX, build A and Y
nt= len(ts)
no= R.shape[0]
#remove 0,0,0 and half-plane
if _isNonAxi(self._pot):
nn= (2*maxn-1)**2*maxn-(maxn-1)*(2*maxn-1)-maxn
else:
nn= maxn*(2*maxn-1)-maxn
A= numpy.zeros((no,nt,2+nn))
A[:,:,0]= 1.
A[:,:,1]= ts
#sorting the phi and Z grids this way makes it easy to exclude the origin
phig= list(numpy.arange(-maxn+1,maxn,1))
phig.sort(key = lambda x: abs(x))
phig= numpy.array(phig,dtype='int')
if _isNonAxi(self._pot):
grid= numpy.meshgrid(numpy.arange(maxn),phig,phig)
else:
grid= numpy.meshgrid(numpy.arange(maxn),phig)
gridR= grid[0].T.flatten()[1:] #remove 0,0,0
gridZ= grid[1].T.flatten()[1:]
mask = numpy.ones(len(gridR),dtype=bool)
# excludes axis that is not in half-space
if _isNonAxi(self._pot):
gridphi= grid[2].T.flatten()[1:]
mask= True\
^(gridR == 0)*((gridphi < 0)+((gridphi==0)*(gridZ < 0)))
else:
mask[:2*maxn-3:2]= False
gridR= gridR[mask]
gridZ= gridZ[mask]
tangleR= numpy.tile(angleRT.T,(nn,1,1)).T
tgridR= numpy.tile(gridR,(no,nt,1))
tangleZ= numpy.tile(angleZT.T,(nn,1,1)).T
tgridZ= numpy.tile(gridZ,(no,nt,1))
if _isNonAxi(self._pot):
gridphi= gridphi[mask]
tgridphi= numpy.tile(gridphi,(no,nt,1))
tanglephi= numpy.tile(anglephiT.T,(nn,1,1)).T
sinnR= numpy.sin(tgridR*tangleR+tgridphi*tanglephi+tgridZ*tangleZ)
else:
sinnR= numpy.sin(tgridR*tangleR+tgridZ*tangleZ)
A[:,:,2:]= sinnR
#Matrix magic
atainv= numpy.empty((no,2+nn,2+nn))
AT= numpy.transpose(A,axes=(0,2,1))
for ii in range(no):
atainv[ii,:,:,]= linalg.inv(numpy.dot(AT[ii,:,:],A[ii,:,:]))
ATAR= numpy.sum(AT*numpy.transpose(numpy.tile(angleRT,(2+nn,1,1)),axes=(1,0,2)),axis=2)
ATAT= numpy.sum(AT*numpy.transpose(numpy.tile(anglephiT,(2+nn,1,1)),axes=(1,0,2)),axis=2)
ATAZ= numpy.sum(AT*numpy.transpose(numpy.tile(angleZT,(2+nn,1,1)),axes=(1,0,2)),axis=2)
angleR= numpy.sum(atainv[:,0,:]*ATAR,axis=1)
OmegaR= numpy.sum(atainv[:,1,:]*ATAR,axis=1)
anglephi= numpy.sum(atainv[:,0,:]*ATAT,axis=1)
Omegaphi= numpy.sum(atainv[:,1,:]*ATAT,axis=1)
angleZ= numpy.sum(atainv[:,0,:]*ATAZ,axis=1)
OmegaZ= numpy.sum(atainv[:,1,:]*ATAZ,axis=1)
Omegaphi[negFreqIndx]= -Omegaphi[negFreqIndx]
anglephi[negFreqIndx]= _TWOPI-anglephi[negFreqIndx]
if kwargs.get('_retacfs',False):
return (jr,lz,jz,OmegaR,Omegaphi,OmegaZ, #pragma: no cover
angleR % _TWOPI,
anglephi % _TWOPI,
angleZ % _TWOPI,acfs)
else:
return (jr,lz,jz,OmegaR,Omegaphi,OmegaZ,
angleR % _TWOPI,
anglephi % _TWOPI,
angleZ % _TWOPI)
def plot(self,*args,**kwargs):
"""
NAME:
plot
PURPOSE:
plot the angles vs. each other, to check whether the isochrone
approximation is good
INPUT:
Either:
a) R,vR,vT,z,vz:
floats: phase-space value for single object
b) Orbit instance
type= ('araz') type of plot to make
a) 'araz': az vs. ar, with color-coded aphi
b) 'araphi': aphi vs. ar, with color-coded az
c) 'azaphi': aphi vs. az, with color-coded ar
d) 'jr': cumulative average of jr with time, to assess convergence
e) 'lz': same as 'jr' but for lz
f) 'jz': same as 'jr' but for jz
deperiod= (False), if True, de-period the angles
downsample= (False) if True, downsample what's plotted to 400 points
+plot kwargs
OUTPUT:
plot to output
HISTORY:
2013-09-10 - Written - Bovy (IAS)
"""
#Kwargs
type= kwargs.pop('type','araz')
deperiod= kwargs.pop('deperiod',False)
downsample= kwargs.pop('downsample',False)
#Parse input
R,vR,vT,z,vz,phi= self._parse_args('a' in type,False,*args)
#Use self._aAI to calculate the actions and angles in the isochrone potential
acfs= self._aAI._actionsFreqsAngles(R.flatten(),
vR.flatten(),
vT.flatten(),
z.flatten(),
vz.flatten(),
phi.flatten())
if type == 'jr' or type == 'lz' or type == 'jz':
jrI= numpy.reshape(acfs[0],R.shape)[:,:-1]
jzI= numpy.reshape(acfs[2],R.shape)[:,:-1]
anglerI= numpy.reshape(acfs[6],R.shape)
anglezI= numpy.reshape(acfs[8],R.shape)
danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1]
danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1]
if True:
sumFunc= numpy.cumsum
jr= sumFunc(jrI*danglerI,axis=1)/sumFunc(danglerI,axis=1)
jz= sumFunc(jzI*danglezI,axis=1)/sumFunc(danglezI,axis=1)
lzI= numpy.reshape(acfs[1],R.shape)[:,:-1]
anglephiI= numpy.reshape(acfs[7],R.shape)
danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1]
lz= sumFunc(lzI*danglephiI,axis=1)/sumFunc(danglephiI,axis=1)
from ..orbit import Orbit
if isinstance(args[0],Orbit) and hasattr(args[0],'t'):
ts= args[0].t[:-1]
else:
ts= self._tsJ[:-1]
if type == 'jr':
if downsample:
plotx= ts[::int(round(self._ntintJ//400))]
ploty= jr[0,::int(round(self._ntintJ//400))]/jr[0,-1]
plotz= anglerI[0,:-1:int(round(self._ntintJ//400))]
else:
plotx= ts
ploty= jr[0,:]/jr[0,-1]
plotz= anglerI[0,:-1]
bovy_plot.bovy_plot(plotx,ploty,
c=plotz,
s=20.,
scatter=True,
edgecolor='none',
xlabel=r'$t$',
ylabel=r'$J^A_R / \langle J^A_R \rangle$',
clabel=r'$\theta^A_R$',
vmin=0.,vmax=2.*numpy.pi,
crange=[0.,2.*numpy.pi],
colorbar=True,
**kwargs)
elif type == 'lz':
if downsample:
plotx= ts[::int(round(self._ntintJ//400))]
ploty= lz[0,::int(round(self._ntintJ//400))]/lz[0,-1]
plotz= anglephiI[0,:-1:int(round(self._ntintJ//400))]
else:
plotx= ts
ploty= lz[0,:]/lz[0,-1]
plotz= anglephiI[0,:-1]
bovy_plot.bovy_plot(plotx,ploty,c=plotz,s=20.,
scatter=True,
edgecolor='none',
xlabel=r'$t$',
ylabel=r'$L^A_Z / \langle L^A_Z \rangle$',
clabel=r'$\theta^A_\phi$',
vmin=0.,vmax=2.*numpy.pi,
crange=[0.,2.*numpy.pi],
colorbar=True,
**kwargs)
elif type == 'jz':
if downsample:
plotx= ts[::int(round(self._ntintJ//400))]
ploty= jz[0,::int(round(self._ntintJ//400))]/jz[0,-1]
plotz= anglezI[0,:-1:int(round(self._ntintJ//400))]
else:
plotx= ts
ploty= jz[0,:]/jz[0,-1]
plotz= anglezI[0,:-1]
bovy_plot.bovy_plot(plotx,ploty,c=plotz,s=20.,
scatter=True,
edgecolor='none',
xlabel=r'$t$',
ylabel=r'$J^A_Z / \langle J^A_Z \rangle$',
clabel=r'$\theta^A_Z$',
vmin=0.,vmax=2.*numpy.pi,
crange=[0.,2.*numpy.pi],
colorbar=True,
**kwargs)
else:
if deperiod:
if 'ar' in type:
angleRT= dePeriod(numpy.reshape(acfs[6],R.shape))
else:
angleRT= numpy.reshape(acfs[6],R.shape)
if 'aphi' in type:
acfs7= numpy.reshape(acfs[7],R.shape)
negFreqIndx= numpy.median(acfs7-numpy.roll(acfs7,1,axis=1),axis=1) < 0. #anglephi is decreasing
anglephiT= numpy.empty(acfs7.shape)
anglephiT[negFreqIndx,:]= dePeriod(_TWOPI-acfs7[negFreqIndx,:])
negFreqPhi= numpy.zeros(R.shape[0],dtype='bool')
negFreqPhi[negFreqIndx]= True
anglephiT[True^negFreqIndx,:]= dePeriod(acfs7[True^negFreqIndx,:])
else:
anglephiT= numpy.reshape(acfs[7],R.shape)
if 'az' in type:
angleZT= dePeriod(numpy.reshape(acfs[8],R.shape))
else:
angleZT= numpy.reshape(acfs[8],R.shape)
xrange= None
yrange= None
else:
angleRT= numpy.reshape(acfs[6],R.shape)
anglephiT= numpy.reshape(acfs[7],R.shape)
angleZT= numpy.reshape(acfs[8],R.shape)
xrange= [-0.5,2.*numpy.pi+0.5]
yrange= [-0.5,2.*numpy.pi+0.5]
vmin, vmax= 0.,2.*numpy.pi
crange= [vmin,vmax]
if type == 'araz':
if downsample:
plotx= angleRT[0,::int(round(self._ntintJ//400))]
ploty= angleZT[0,::int(round(self._ntintJ//400))]
plotz= anglephiT[0,::int(round(self._ntintJ//400))]
else:
plotx= angleRT[0,:]
ploty= angleZT[0,:]
plotz= anglephiT[0,:]
bovy_plot.bovy_plot(plotx,ploty,c=plotz,s=20.,
scatter=True,
edgecolor='none',
xlabel=r'$\theta^A_R$',
ylabel=r'$\theta^A_Z$',
clabel=r'$\theta^A_\phi$',
xrange=xrange,yrange=yrange,
vmin=vmin,vmax=vmax,
crange=crange,
colorbar=True,
**kwargs)
elif type == 'araphi':
if downsample:
plotx= angleRT[0,::int(round(self._ntintJ//400))]
ploty= anglephiT[0,::int(round(self._ntintJ//400))]
plotz= angleZT[0,::int(round(self._ntintJ//400))]
else:
plotx= angleRT[0,:]
ploty= anglephiT[0,:]
plotz= angleZT[0,:]
bovy_plot.bovy_plot(plotx,ploty,c=plotz,s=20.,
scatter=True,
edgecolor='none',
xlabel=r'$\theta^A_R$',
clabel=r'$\theta^A_Z$',
ylabel=r'$\theta^A_\phi$',
xrange=xrange,yrange=yrange,
vmin=vmin,vmax=vmax,
crange=crange,
colorbar=True,
**kwargs)
elif type == 'azaphi':
if downsample:
plotx= angleZT[0,::int(round(self._ntintJ//400))]
ploty= anglephiT[0,::int(round(self._ntintJ//400))]
plotz= angleRT[0,::int(round(self._ntintJ//400))]
else:
plotx= angleZT[0,:]
ploty= anglephiT[0,:]
plotz= angleRT[0,:]
bovy_plot.bovy_plot(plotx,ploty,c=plotz,s=20.,
scatter=True,
edgecolor='none',
clabel=r'$\theta^A_R$',
xlabel=r'$\theta^A_Z$',
ylabel=r'$\theta^A_\phi$',
xrange=xrange,yrange=yrange,
vmin=vmin,vmax=vmax,
crange=crange,
colorbar=True,
**kwargs)
return None
def _parse_args(self,freqsAngles=True,_firstFlip=False,*args):
"""Helper function to parse the arguments to the __call__ and actionsFreqsAngles functions"""
from ..orbit import Orbit
RasOrbit= False
integrated= True #whether the orbit was already integrated when given
if len(args) == 5 or len(args) == 3: #pragma: no cover
raise IOError("Must specify phi for actionAngleIsochroneApprox")
if len(args) == 6 or len(args) == 4:
if len(args) == 6:
R,vR,vT, z, vz, phi= args
else:
R,vR,vT, phi= args
z, vz= 0., 0.
if isinstance(R,float):
os= [Orbit([R,vR,vT,z,vz,phi])]
RasOrbit= True
integrated= False
elif len(R.shape) == 1: #not integrated yet
os= [Orbit([R[ii],vR[ii],vT[ii],z[ii],vz[ii],phi[ii]]) for ii in range(R.shape[0])]
RasOrbit= True
integrated= False
if isinstance(args[0],Orbit) \
or (isinstance(args[0],list) and isinstance(args[0][0],Orbit)) \
or RasOrbit:
if RasOrbit:
pass
elif not isinstance(args[0],list):
os= [args[0]]
if os[0].phasedim() == 3 or os[0].phasedim() == 5: #pragma: no cover
raise IOError("Must specify phi for actionAngleIsochroneApprox")
else:
os= args[0]
if os[0].phasedim() == 3 or os[0].phasedim() == 5: #pragma: no cover
raise IOError("Must specify phi for actionAngleIsochroneApprox")
self._check_consistent_units_orbitInput(os[0])
if not hasattr(os[0],'orbit'): #not integrated yet
if _firstFlip:
for o in os:
o.vxvv[...,1]= -o.vxvv[...,1]
o.vxvv[...,2]= -o.vxvv[...,2]
o.vxvv[...,4]= -o.vxvv[...,4]
[o.integrate(self._tsJ,pot=self._pot,
method=self._integrate_method,
dt=self._integrate_dt) for o in os]
if _firstFlip:
for o in os:
o.vxvv[...,1]= -o.vxvv[...,1]
o.vxvv[...,2]= -o.vxvv[...,2]
o.vxvv[...,4]= -o.vxvv[...,4]
o.orbit[...,1]= -o.orbit[...,1]
o.orbit[...,2]= -o.orbit[...,2]
o.orbit[...,4]= -o.orbit[...,4]
integrated= False
ntJ= os[0].getOrbit().shape[0]
no= len(os)
R= numpy.empty((no,ntJ))
vR= numpy.empty((no,ntJ))
vT= numpy.empty((no,ntJ))
z= numpy.zeros((no,ntJ))+10.**-7. #To avoid numpy warnings for
vz= numpy.zeros((no,ntJ))+10.**-7. #planarOrbits
phi= numpy.empty((no,ntJ))
for ii in range(len(os)):
this_orbit= os[ii].getOrbit()
R[ii,:]= this_orbit[:,0]
vR[ii,:]= this_orbit[:,1]
vT[ii,:]= this_orbit[:,2]
if this_orbit.shape[1] == 6:
z[ii,:]= this_orbit[:,3]
vz[ii,:]= this_orbit[:,4]
phi[ii,:]= this_orbit[:,5]
else:
phi[ii,:]= this_orbit[:,3]
if freqsAngles and not integrated: #also integrate backwards in time, such that the requested point is not at the edge
no= R.shape[0]
nt= R.shape[1]
oR= numpy.empty((no,2*nt-1))
ovR= numpy.empty((no,2*nt-1))
ovT= numpy.empty((no,2*nt-1))
oz= numpy.zeros((no,2*nt-1))+10.**-7. #To avoid numpy warnings for
ovz= numpy.zeros((no,2*nt-1))+10.**-7. #planarOrbits
ophi= numpy.empty((no,2*nt-1))
if _firstFlip:
oR[:,:nt]= R[:,::-1]
ovR[:,:nt]= vR[:,::-1]
ovT[:,:nt]= vT[:,::-1]
oz[:,:nt]= z[:,::-1]
ovz[:,:nt]= vz[:,::-1]
ophi[:,:nt]= phi[:,::-1]
else:
oR[:,nt-1:]= R
ovR[:,nt-1:]= vR
ovT[:,nt-1:]= vT
oz[:,nt-1:]= z
ovz[:,nt-1:]= vz
ophi[:,nt-1:]= phi
#load orbits
if _firstFlip:
os= [Orbit([R[ii,0],vR[ii,0],vT[ii,0],z[ii,0],vz[ii,0],phi[ii,0]]) for ii in range(R.shape[0])]
else:
os= [Orbit([R[ii,0],-vR[ii,0],-vT[ii,0],z[ii,0],-vz[ii,0],phi[ii,0]]) for ii in range(R.shape[0])]
#integrate orbits
[o.integrate(self._tsJ,pot=self._pot,
method=self._integrate_method,
dt=self._integrate_dt) for o in os]
#extract phase-space points along the orbit
ts= self._tsJ
if _firstFlip:
for ii in range(no):
oR[ii,nt:]= os[ii].R(ts[1:]) #drop t=0, which we have
ovR[ii,nt:]= os[ii].vR(ts[1:]) #already
ovT[ii,nt:]= os[ii].vT(ts[1:]) # reverse, such that
if os[ii].getOrbit().shape[1] == 6:
oz[ii,nt:]= os[ii].z(ts[1:]) #everything is in the
ovz[ii,nt:]= os[ii].vz(ts[1:]) #right order
ophi[ii,nt:]= os[ii].phi(ts[1:]) #!
else:
for ii in range(no):
oR[ii,:nt-1]= os[ii].R(ts[1:])[::-1] #drop t=0, which we have
ovR[ii,:nt-1]= -os[ii].vR(ts[1:])[::-1] #already
ovT[ii,:nt-1]= -os[ii].vT(ts[1:])[::-1] # reverse, such that
if os[ii].getOrbit().shape[1] == 6:
oz[ii,:nt-1]= os[ii].z(ts[1:])[::-1] #everything is in the
ovz[ii,:nt-1]= -os[ii].vz(ts[1:])[::-1] #right order
ophi[ii,:nt-1]= os[ii].phi(ts[1:])[::-1] #!
return (oR,ovR,ovT,oz,ovz,ophi)
else:
return (R,vR,vT,z,vz,phi)
@potential_physical_input
@physical_conversion('position',pop=True)
def estimateBIsochrone(pot,R,z,phi=None):
"""
NAME:
estimateBIsochrone
PURPOSE:
Estimate a good value for the scale of the isochrone potential by matching the slope of the rotation curve
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)
phi= (None) azimuth to use for non-axisymmetric potentials (array if R and z are arrays)
OUTPUT:
b if 1 R,Z given
bmin,bmedian,bmax if multiple R given
HISTORY:
2013-09-12 - Written - Bovy (IAS)
2016-02-20 - Changed input order to allow physical conversions - Bovy (UofT)
2016-06-28 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT)
"""
if pot is None: #pragma: no cover
raise IOError("pot= needs to be set to a Potential instance or list thereof")
if isinstance(R,numpy.ndarray):
if phi is None: phi= [None for r in R]
bs= numpy.array([estimateBIsochrone(pot,R[ii],z[ii],phi=phi[ii],
use_physical=False)
for ii in range(len(R))])
return numpy.array([numpy.amin(bs[True^numpy.isnan(bs)]),
numpy.median(bs[True^numpy.isnan(bs)]),
numpy.amax(bs[True^numpy.isnan(bs)])])
else:
r2= R**2.+z**2
r= numpy.sqrt(r2)
dlvcdlr= dvcircdR(pot,r,phi=phi,use_physical=False)/vcirc(pot,r,phi=phi,use_physical=False)*r
try:
b= optimize.brentq(lambda x: dlvcdlr-(x/numpy.sqrt(r2+x**2.)-0.5*r2/(r2+x**2.)),
0.01,100.)
except: #pragma: no cover
b= numpy.nan
return b
def dePeriod(arr):
"""make an array of periodic angles increase linearly"""
diff= arr-numpy.roll(arr,1,axis=1)
w= diff < -6.
addto= numpy.cumsum(w.astype(int),axis=1)
return arr+_TWOPI*addto