From 676c11e8a4894d4929f8d277f4333d1ccfb47424 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 9 Jul 2020 12:14:43 -0400 Subject: [PATCH 01/91] Add basic structure for implementing spherical distribution functions --- galpy/df/Eddingtondf.py | 17 +++++++++++ galpy/df/__init__.py | 6 ++++ galpy/df/constantbetaHernquistdf.py | 14 +++++++++ galpy/df/constantbetadf.py | 12 ++++++++ galpy/df/isotropicHernquistdf.py | 18 ++++++++++++ galpy/df/sphericaldf.py | 45 +++++++++++++++++++++++++++++ tests/test_sphericaldf.py | 6 ++++ 7 files changed, 118 insertions(+) create mode 100644 galpy/df/Eddingtondf.py create mode 100644 galpy/df/constantbetaHernquistdf.py create mode 100644 galpy/df/constantbetadf.py create mode 100644 galpy/df/isotropicHernquistdf.py create mode 100644 galpy/df/sphericaldf.py create mode 100644 tests/test_sphericaldf.py diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py new file mode 100644 index 000000000..3d4ba6cfb --- /dev/null +++ b/galpy/df/Eddingtondf.py @@ -0,0 +1,17 @@ +# Class that implements isotropic spherical DFs computed using the Eddington +# formula +from .sphericaldf import sphericaldf + +class Eddingtondf(sphericaldf): + """Class that implements isotropic spherical DFs computed using the Eddington formula""" + def __init__(self,ro=None,vo=None): + sphericaldf.__init__(self,ro=ro,vo=vo) + + def fE(self,E): + # Stub for computing f(E) + return None + + def _sample_eta(self): + # Stub for function that samples eta + return None + diff --git a/galpy/df/__init__.py b/galpy/df/__init__.py index 60408b7ad..4c47d298c 100644 --- a/galpy/df/__init__.py +++ b/galpy/df/__init__.py @@ -5,6 +5,9 @@ from . import streamdf from . import streamgapdf from . import jeans +from . import Eddingtondf +from . import isotropicHernquistdf +from . import constantbetaHernquistdf # # Functions # @@ -32,3 +35,6 @@ quasiisothermaldf= quasiisothermaldf.quasiisothermaldf streamdf= streamdf.streamdf streamgapdf= streamgapdf.streamgapdf +Eddingtondf= Eddingtondf.Eddingtondf +isotropicHernquistdf= isotropicHernquistdf.isotropicHernquistdf +constantbetaHernquistdf= constantbetaHernquistdf.constantbetaHernquistdf diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py new file mode 100644 index 000000000..9a7ab3189 --- /dev/null +++ b/galpy/df/constantbetaHernquistdf.py @@ -0,0 +1,14 @@ +# Class that implements the anisotropic spherical Hernquist DF with constant +# beta parameter +from .constantbetadf import constantbetadf + +class constantbetaHernquistdf(constantbetadf): + """Class that implements the anisotropic spherical Hernquist DF with constant beta parameter""" + def __init__(self,ro=None,vo=None): + constantbetadf.__init__(self,ro=ro,vo=vo) + + def f1E(self,E): + # Stub for computing f_1(E) + return None + + diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py new file mode 100644 index 000000000..8a60ce59f --- /dev/null +++ b/galpy/df/constantbetadf.py @@ -0,0 +1,12 @@ +# Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant +# beta anisotropy parameter +from .sphericaldf import anisotropicsphericaldf + +class constantbetadf(anisotropicsphericaldf): + """Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant beta anisotropy parameter""" + def __init__(self,ro=None,vo=None): + anisotropicsphericaldf.__init__(self,ro=ro,vo=vo) + + def f1E(self,E): + # Stub for computing f_1(E) in BT08 nomenclature + return None diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py new file mode 100644 index 000000000..f56ba5553 --- /dev/null +++ b/galpy/df/isotropicHernquistdf.py @@ -0,0 +1,18 @@ +# Class that implements isotropic spherical Hernquist DF +# computed using the Eddington formula +from .sphericaldf import sphericaldf +from .Eddingtondf import Eddingtondf + +class isotropicHernquistdf(Eddingtondf): + """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" + def __init__(self,ro=None,vo=None): + # Initialize using sphericaldf rather than Eddingtondf, because + # Eddingtondf will have code specific to computing the Eddington + # integral, which is not necessary for Hernquist + sphericaldf.__init__(self,ro=ro,vo=vo) + + def fE(self,E): + # Stub for computing f(E) + return None + + diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py new file mode 100644 index 000000000..ee56dbc40 --- /dev/null +++ b/galpy/df/sphericaldf.py @@ -0,0 +1,45 @@ +# Superclass for spherical distribution functions, contains +# - sphericaldf: superclass of all spherical DFs +# - anisotropicsphericaldf: superclass of all anisotropic spherical DFs +from .df import df + +class sphericaldf(df): + """Superclass for spherical distribution functions""" + def __init__(self,ro=None,vo=None): + df.__init__(self,ro=ro,vo=vo) + +############################## EVALUATING THE DF############################### + def __call__(self,*args,**kwargs): + # Stub for calling the DF as a function of either a) R,vR,vT,z,vz,phi, + # b) Orbit, c) E,L (Lz?) --> maybe depends on the actual form? + return None + +############################### SAMPLING THE DF################################ + def sample(self): + # Stub for main sampling function, which will return (x,v) or Orbits... + return None + + def _sample_r(self,n=1): + # Stub for sampling the radius from M( Date: Fri, 10 Jul 2020 11:59:40 -0400 Subject: [PATCH 02/91] Basic solution of the King DF density --- galpy/df/kingdf.py | 130 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 galpy/df/kingdf.py diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py new file mode 100644 index 000000000..46c93a87d --- /dev/null +++ b/galpy/df/kingdf.py @@ -0,0 +1,130 @@ +# Class that represents a King DF +import numpy +from scipy import special, integrate, interpolate +from .sphericaldf import sphericaldf + +_FOURPI= 4.*numpy.pi +_TWOOVERSQRTPI= 2./numpy.sqrt(numpy.pi) + +class kingdf(sphericaldf): + """Class that represents a King DF""" + def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize a King DF + + INPUT: + + W0 - dimensionless central potential W0 = Psi(0)/sigma^2 (in practice, needs to be <~ 200, where the DF is essentially isothermal) + + M= (1.) total mass (can be a Quantity) + + rt= (1.) tidal radius (can be a Quantity) + + npt= (1001) number of points to use to solve for Psi(r) + + ro=, vo= standard galpy unit scaling parameters + + OUTPUT: + + (none; sets up instance) + + HISTORY: + + 2020-07-09 - Written - Bovy (UofT) + + """ + sphericaldf.__init__(self,ro=ro,vo=vo) + # Need to add parsing of Quantity inputs... + + self.W0= W0 + # Solve (mass,rtidal)-scale-free model + self._scalefree_kdf= _scalefreekingdf(self.W0) + self._scalefree_kdf.solve(npt) + # Set up scaling factors + self._radius_scale= rt/self._scalefree_kdf.rt + self._mass_scale= M/self._scalefree_kdf.mass + self._velocity_scale= numpy.sqrt(self._mass_scale/self._radius_scale) + self._density_scale= self._mass_scale/self._radius_scale**3. + # Store central density, r0... + self.rho0= self._scalefree_kdf.rho0*self._density_scale + self.r0= self._scalefree_kdf.r0*self._radius_scale + self.c= self._scalefree_kdf.c # invariant + + def dens(self,r): + return self._scalefree_kdf.dens(r/self._radius_scale)\ + *self._density_scale + +class _scalefreekingdf(object): + """Internal helper class to solve the scale-free King DF model""" + def __init__(self,W0): + self.W0= W0 + + def solve(self,npt=1001): + """Solve the model W(r) at npt points (note: not equally spaced in + either r or W, because combination of two ODEs for different r ranges)""" + # Set up arrays for outputs + r= numpy.zeros(npt) + W= numpy.zeros(npt) + m= numpy.zeros(npt) + # Initialize (r[0]=0 already) + W[0]= self.W0 + # Determine central density and r0 + self.rho0= self._dens_W(self.W0) + self.r0= numpy.sqrt(9./4./numpy.pi/self.rho0) + # First solve Poisson equation ODE from r=0 to r0 using form + # d^2 Psi / dr^2 = ... (d psi / dr = v, r^2 dv / dr = RHS-2*r*v) + if self.W0 < 2.: + rbreak= self.r0/100. + else: + rbreak= self.r0 + #Using linspace focuses on what happens ~rbreak rather than on < 0. else 0.)], + [0.,rbreak],[self.W0,0.],method='LSODA',t_eval=r[:npt//2]) + W[:npt//2]= sol.y[0] + # Then solve Poisson equation ODE from Psi(r0) to Psi=0 using form + # d^2 r / d Psi^2 = ... (d r / d psi = 1/v, dv / dpsi = 1/v(RHS-2*r*v)) + # Added advantage that this becomes ~log-spaced in r, which is what + # you want + W[npt//2-1:]= numpy.linspace(sol.y[0,-1],0.,npt-npt//2+1) + sol= integrate.solve_ivp(\ + lambda t,y: [1./y[1], + -1./y[1]*(_FOURPI*self._dens_W(t) + +2.*y[1]/y[0])], + [sol.y[0,-1],0.],[rbreak,sol.y[1,-1]], + method='LSODA',t_eval=W[npt//2-1:]) + r[npt//2-1:]= sol.y[0] + # Store solution + self._r= r + self._W= W + # Also store density at these points, and the tidal radius + self._rho= self._dens_W(self._W) + self.rt= r[-1] + self.c= numpy.log10(self.rt/self.r0) + # Interpolate solution + self._W_from_r=\ + interpolate.InterpolatedUnivariateSpline(self._r,self._W,k=3) + # Compute the cumulative mass and store the total mass + mass_shells= numpy.array([\ + integrate.quad(lambda r: _FOURPI*r**2*self.dens(r), + rlo,rhi)[0] for rlo,rhi in zip(r[:-1],r[1:])]) + self._cumul_mass= numpy.cumsum(mass_shells) + self.mass= self._cumul_mass[-1] + return None + + def _dens_W(self,W): + """Density as a function of W""" + sqW= numpy.sqrt(W) + return numpy.exp(W)*special.erf(sqW)-_TWOOVERSQRTPI*sqW*(1.+2./3.*W) + + def dens(self,r): + return self._dens_W(self._W_from_r(r)) From 4a52ada4a5d4801ed9ff266813442cef16d235a7 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Sun, 12 Jul 2020 20:50:41 -0400 Subject: [PATCH 03/91] Better in-code comments [ci skip] --- galpy/df/kingdf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 46c93a87d..79859ce2e 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -43,7 +43,8 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): # Need to add parsing of Quantity inputs... self.W0= W0 - # Solve (mass,rtidal)-scale-free model + # Solve (mass,rtidal)-scale-free model, which is the basis for + # the full solution self._scalefree_kdf= _scalefreekingdf(self.W0) self._scalefree_kdf.solve(npt) # Set up scaling factors @@ -55,13 +56,14 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): self.rho0= self._scalefree_kdf.rho0*self._density_scale self.r0= self._scalefree_kdf.r0*self._radius_scale self.c= self._scalefree_kdf.c # invariant + self.rt= rt # for convenience def dens(self,r): return self._scalefree_kdf.dens(r/self._radius_scale)\ *self._density_scale class _scalefreekingdf(object): - """Internal helper class to solve the scale-free King DF model""" + """Internal helper class to solve the scale-free King DF model, that is, the one that only depends on W = Psi/sigma^2""" def __init__(self,W0): self.W0= W0 From 41d6ffd41bc98881bcdb00d1590c9a01075ee6a3 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Tue, 14 Jul 2020 21:21:59 -0400 Subject: [PATCH 04/91] Also store dW/dr for use in setting up KingPotential --- galpy/df/kingdf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 79859ce2e..02b5c9108 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -73,6 +73,7 @@ def solve(self,npt=1001): # Set up arrays for outputs r= numpy.zeros(npt) W= numpy.zeros(npt) + dWdr= numpy.zeros(npt) m= numpy.zeros(npt) # Initialize (r[0]=0 already) W[0]= self.W0 @@ -93,6 +94,7 @@ def solve(self,npt=1001): -(2.*y[1]/t if t > 0. else 0.)], [0.,rbreak],[self.W0,0.],method='LSODA',t_eval=r[:npt//2]) W[:npt//2]= sol.y[0] + dWdr[:npt//2]= sol.y[1] # Then solve Poisson equation ODE from Psi(r0) to Psi=0 using form # d^2 r / d Psi^2 = ... (d r / d psi = 1/v, dv / dpsi = 1/v(RHS-2*r*v)) # Added advantage that this becomes ~log-spaced in r, which is what @@ -105,9 +107,11 @@ def solve(self,npt=1001): [sol.y[0,-1],0.],[rbreak,sol.y[1,-1]], method='LSODA',t_eval=W[npt//2-1:]) r[npt//2-1:]= sol.y[0] + dWdr[npt//2-1:]= sol.y[1] # Store solution self._r= r self._W= W + self._dWdr= dWdr # Also store density at these points, and the tidal radius self._rho= self._dens_W(self._W) self.rt= r[-1] From 50a667969581ba068a7c894ce2573734a9c6ca0f Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Tue, 14 Jul 2020 21:22:21 -0400 Subject: [PATCH 05/91] Add Potential for the King model, using interpSphericalPotential --- galpy/potential/KingPotential.py | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 galpy/potential/KingPotential.py diff --git a/galpy/potential/KingPotential.py b/galpy/potential/KingPotential.py new file mode 100644 index 000000000..7f8342f5b --- /dev/null +++ b/galpy/potential/KingPotential.py @@ -0,0 +1,62 @@ +############################################################################### +# KingPotential.py: Potential of a King profile +############################################################################### +import numpy +from .interpSphericalPotential import interpSphericalPotential +class KingPotential(interpSphericalPotential): + """KingPotential.py: Potential of a King profile, defined from the distribution function + + .. math:: + + f(\\mathcal{E}) = \begin{cases} \\rho_1\\,(2\\pi\\sigma^2)^{-3/2}\\,\\left(e^{\\mathcal{E}/\\sigma^2}-1\\right), & \mathcal{E} > 0\\ +0, & \mathcal{E} \leq 0\end{cases} + + where :math:`\mathcal{E}` is the binding energy. + """ + def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize a King potential + + INPUT: + + W0= (2.) dimensionless central potential W0 = Psi(0)/sigma^2 (in practice, needs to be <~ 200, where the DF is essentially isothermal) + + M= (1.) total mass (can be a Quantity) + + rt= (1.) tidal radius (can be a Quantity) + + npt= (1001) number of points to use to solve for Psi(r) when solving the King DF + + scfa= (1.) scale parameter used in the SCF representation of the potential + + scfN= (30) number of expansion coefficients to use in the SCF representation of the potential + + ro=, vo= standard galpy unit scaling parameters + + OUTPUT: + + (none; sets up instance) + + HISTORY: + + 2020-07-11 - Written - Bovy (UofT) + + """ + # Set up King DF + from ..df.kingdf import kingdf + kdf= kingdf(W0,M=M,rt=rt,ro=ro,vo=vo) + interpSphericalPotential.__init__(\ + self, + rforce=lambda r: kdf._mass_scale/kdf._radius_scale**2. + *numpy.interp(r/kdf._radius_scale, + kdf._scalefree_kdf._r, + kdf._scalefree_kdf._dWdr), + rgrid=kdf._scalefree_kdf._r*kdf._radius_scale, + Phi0=-kdf.W0*kdf._mass_scale/kdf._radius_scale, + ro=ro,vo=vo) From 06767ddee9adfee977d4bd217d10a8e28127714e Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 15 Jul 2020 20:39:40 -0400 Subject: [PATCH 06/91] Add KingPotential to top level --- galpy/potential/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/galpy/potential/__init__.py b/galpy/potential/__init__.py index ad690b8ef..e67724642 100644 --- a/galpy/potential/__init__.py +++ b/galpy/potential/__init__.py @@ -49,6 +49,7 @@ from . import NumericalPotentialDerivativesMixin from . import HomogeneousSpherePotential from . import interpSphericalPotential +from . import KingPotential # # Functions # @@ -166,6 +167,7 @@ NumericalPotentialDerivativesMixin= NumericalPotentialDerivativesMixin.NumericalPotentialDerivativesMixin HomogeneousSpherePotential= HomogeneousSpherePotential.HomogeneousSpherePotential interpSphericalPotential= interpSphericalPotential.interpSphericalPotential +KingPotential= KingPotential.KingPotential #Wrappers DehnenSmoothWrapperPotential= DehnenSmoothWrapperPotential.DehnenSmoothWrapperPotential SolidBodyRotationWrapperPotential= SolidBodyRotationWrapperPotential.SolidBodyRotationWrapperPotential From eeb5f964bf6f1da70ce8b25e1a7db36fb37017ef Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 15 Jul 2020 21:41:12 -0400 Subject: [PATCH 07/91] Slightly loosen Liouville test tolerance for KingPotential --- tests/test_orbit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_orbit.py b/tests/test_orbit.py index 9e4177804..3e282989d 100644 --- a/tests/test_orbit.py +++ b/tests/test_orbit.py @@ -706,6 +706,7 @@ def test_liouville_planar(): tol['triaxialLogarithmicHaloPotential']= -7. #more difficult tol['FerrersPotential']= -2. tol['HomogeneousSpherePotential']= -4. + tol['KingPotential']= -6. tol['mockInterpSphericalPotential']= -4. # == HomogeneousSpherePotential tol['mockFlatCosmphiDiskwBreakPotential']= -7. # more difficult tol['mockFlatTrulyCorotatingRotationSpiralArmsPotential']= -5. # more difficult From 30f147bffce381e8053547b4bbd177b8c3a4d39f Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:35:56 -0700 Subject: [PATCH 08/91] Add __init__ and imports for constantbetaHernquistdf --- galpy/df/constantbetaHernquistdf.py | 37 +++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 9a7ab3189..059f8e955 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -1,11 +1,44 @@ # Class that implements the anisotropic spherical Hernquist DF with constant # beta parameter +import numpy +import pdb +import scipy.special +import scipy.integrate from .constantbetadf import constantbetadf +from .df import _APY_LOADED +from ..potential import evaluatePotentials,HernquistPotential +if _APY_LOADED: + from astropy import units class constantbetaHernquistdf(constantbetadf): """Class that implements the anisotropic spherical Hernquist DF with constant beta parameter""" - def __init__(self,ro=None,vo=None): - constantbetadf.__init__(self,ro=ro,vo=vo) + def __init__(self,pot=None,beta=0,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize a DF with constant anisotropy + + INPUT: + + pot - Hernquist potential which determines the DF + + beta - anisotropy parameter + + OUTPUT: + + None + + HISTORY: + + 2020-07-22 - Written + """ + assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' + constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) + def f1E(self,E): # Stub for computing f_1(E) From 45b51a3f14805857deba3a81d35085edd1a9e1c8 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:36:21 -0700 Subject: [PATCH 09/91] Add __call_internal__ for constantbetaHernquistdf --- galpy/df/constantbetaHernquistdf.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 059f8e955..5c6fea830 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -39,6 +39,34 @@ def __init__(self,pot=None,beta=0,ro=None,vo=None): assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) + def __call_internal__(self,*args): + """ + NAME: + + __call_internal + + PURPOSE: + + Evaluate the DF for a constant anisotropy Hernquist + + INPUT: + + E - The energy + + L - The angular momentum + + OUTPUT: + + fH - The value of the DF + + HISTORY: + + 2020-07-22 - Written + """ + E = args[0] + L = args[1] + f1 = self.f1E(E) + return L**(-2*self.beta)*f1 def f1E(self,E): # Stub for computing f_1(E) From 5fdaa967ad6e28ed055a058405f8255479e02d43 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:36:47 -0700 Subject: [PATCH 10/91] Add functions for calculating f1E --- galpy/df/constantbetaHernquistdf.py | 78 ++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 5c6fea830..fd933a5a5 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -69,7 +69,81 @@ def __call_internal__(self,*args): return L**(-2*self.beta)*f1 def f1E(self,E): - # Stub for computing f_1(E) - return None + """ + NAME: + + f1E + + PURPOSE + + Calculate the energy portion of a Hernquist distribution function + + INPUT: + + E - The energy (can be Quantity) + OUTPUT: + + f1E - The value of the energy portion of the DF + + HISTORY: + + 2020-07-22 - Written + """ + if _APY_LOADED and isinstance(E,units.quantity.Quantity): + E.to(units.km**2/units.s**2).value/vo**2. + # Scale energies + phi0 = evaluatePotentials(self._pot,0,0) + Erel = -E + Etilde = Erel/phi0 + # Handle potential E outside of bounds + Etilde_out = numpy.where(Etilde<0|Etilde>1)[0] + if len(Etilde_out)>0: + # Set to dummy and NaN later, wierd but prevents functions throwing errors + Etilde[Etilde_out]=0.5 + + # Evaluate depending on beta + _GMa = phi0*self._pot.a**2. + if self.beta == 0.: + f1 = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ + *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ + *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ + +((3*numpy.arcsin(numpy.sqrt(Etilde)))/numpy.sqrt(Etilde*(1-Etilde)))) + elif self.beta == 0.5: + f1 = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) + elif self.beta == -0.5: + f1 = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ + /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) + elif self.beta < 1.0 and self.beta > 0.5: + f1 = self._f1_beta_gt05_Hernquist(Erel) + else: + f1 = self._f1_any_beta(Erel) # This function sits in the super class? + + + return f1 + + def _f1_beta_gt05_Hernquist(self,Erel): + """Calculate f1 for a Hernquist model when 0.5 < beta < 1.0""" + psi0 = evaluatePotentials(self._pot,0,0) + _a = self._pot.a + _GM = psi0*_a + Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self.beta)\ + /scipy.special.gamma(1.5-self.beta) + Cbeta = 2**(self.beta-0.5)/(2*numpy.pi*Ibeta) + alpha = self.beta-0.5 + coeff = (Cbeta*_a**(2*self.beta-2))*(numpy.sin(alpha*numpy.pi))\ + /(_GM*2*numpy.pi**2) + integral = scipy.integrate.quad(self.f1_beta_gt05_integral, + a=0, b=Erel, args=(Erel,psi0) )[0] + return coeff*integral + def _f1_beta_gt05_integral_Hernquist(self,psi,Erel,psi0): + """Integral for calculating f1 for a Hernquist when 0.5 < beta < 1.0""" + psiTilde = psi/psi0 + # Absolute value because the answer normally comes out imaginary? + denom = numpy.abs( (Erel-psi)**(1.5-self.beta) ) + numer = ((4-2*self.beta)*numpy.power(1-psiTilde,2*self.beta-1)\ + *numpy.power(psiTilde,3-2*self.beta)+(1-2*self.beta)\ + *numpy.power(1-psiTilde,2*self.beta-2)\ + *numpy.power(psiTilde,4-2*self.beta)) + return numer/denom From 3d0002ed9b13f87b3878334863738e681c253226 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:37:03 -0700 Subject: [PATCH 11/91] Add __init__ and imports for constantbetadf --- galpy/df/constantbetadf.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 8a60ce59f..38b1799b2 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -4,8 +4,23 @@ class constantbetadf(anisotropicsphericaldf): """Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant beta anisotropy parameter""" - def __init__(self,ro=None,vo=None): - anisotropicsphericaldf.__init__(self,ro=ro,vo=vo) + def __init__(self,pot=None,beta=None,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize a spherical DF with constant anisotropy parameter + + INPUT: + + pot - Spherical potential which determines the DF + """ + anisotropicsphericaldf.__init__(self,pot=pot,dftype='constant', + ro=ro,vo=vo) + self.beta = beta def f1E(self,E): # Stub for computing f_1(E) in BT08 nomenclature From 9ff6f01f7319270f66e2cc32aa5201ad53228e8f Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:38:05 -0700 Subject: [PATCH 12/91] Add __init__ and __call_internal__ for isotropicHernquistdf --- galpy/df/isotropicHernquistdf.py | 52 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index f56ba5553..aba37e999 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -5,14 +5,56 @@ class isotropicHernquistdf(Eddingtondf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" - def __init__(self,ro=None,vo=None): + def __init__(self,pot=None,ro=None,vo=None): # Initialize using sphericaldf rather than Eddingtondf, because # Eddingtondf will have code specific to computing the Eddington # integral, which is not necessary for Hernquist - sphericaldf.__init__(self,ro=ro,vo=vo) + sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) - def fE(self,E): - # Stub for computing f(E) - return None + def __call_internal__(self,*args): + """ + NAME: + __call_internal__ + + PURPOSE + + Calculate the distribution function for an isotropic Hernquist + + INPUT: + + E - The energy + + OUTPUT: + + fH - The distribution function + + HISTORY: + + 2020-07 - Written + + """ + E = args[0] + if _APY_LOADED and isinstance(E,units.quantity.Quantity): + E.to(units.km**2/units.s**2).value/vo**2. + # Scale energies + phi0 = evaluatePotentials(self._pot,0,0) + Erel = -E + Etilde = Erel/phi0 + # Handle potential E out of bounds + Etilde_out = numpy.where(Etilde<0|Etilde>1)[0] + if len(Etilde_out)>0: + # Set to dummy and 0 later, prevents functions throwing errors? + Etilde[Etilde_out]=0.5 + _GMa = phi0*self._pot.a**2. + fH = numpy.power((2**0.5)*((2*numpy.pi)**3) *((_GMa)**1.5),-1)\ + *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ + *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ + +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ + /numpy.sqrt(Etilde*(1-Etilde)))) + # Fix out of bounds values + if len(Etilde_out) > 0: + fH[Etilde_out] = 0 + return fH + From 164860b961c66d80008010647c0b5f448fd820b4 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:38:19 -0700 Subject: [PATCH 13/91] Imports for sphericaldf --- galpy/df/sphericaldf.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index ee56dbc40..923a96551 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -1,7 +1,16 @@ # Superclass for spherical distribution functions, contains # - sphericaldf: superclass of all spherical DFs # - anisotropicsphericaldf: superclass of all anisotropic spherical DFs -from .df import df +import numpy +import pdb +import scipy.interpolate +from .df import df, _APY_LOADED +from ..potential import flatten as flatten_potential +from ..potential import evaluatePotentials +from ..orbit import Orbit +from ..util.bovy_conversion import physical_conversion +if _APY_LOADED: + from astropy import units class sphericaldf(df): """Superclass for spherical distribution functions""" @@ -42,4 +51,3 @@ class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" def __init__(self,ro=None,vo=None): sphericaldf.__init__(self,ro=ro,vo=vo) - From bf06f7b1b59d3a0bf1c70b26a767eb5beb8fef40 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:38:49 -0700 Subject: [PATCH 14/91] Add __init__ for sphericaldf --- galpy/df/sphericaldf.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 923a96551..afddd75bf 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -14,8 +14,34 @@ class sphericaldf(df): """Superclass for spherical distribution functions""" - def __init__(self,ro=None,vo=None): + def __init__(self,pot=None,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initializes a spherical DF + + INPUT: + + pot - Spherical potential which determines the DF + + OUTPUT: + + None + + HISTORY: + + 2020-07-22 - Written - + + """ df.__init__(self,ro=ro,vo=vo) + if pot is None: + raise IOError("pot= must be set") + # Some sort of check for spherical symmetry in the potential? + self._pot = flatten_potential(pot) ############################## EVALUATING THE DF############################### def __call__(self,*args,**kwargs): @@ -50,4 +76,4 @@ def _sample_velocity_angles(self,r,n=1): class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" def __init__(self,ro=None,vo=None): - sphericaldf.__init__(self,ro=ro,vo=vo) + sphericaldf.__init__(self,ro=ro,vo=vo) \ No newline at end of file From 1be08c17bae0a50df9c8987eef3df07b202da877 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:39:14 -0700 Subject: [PATCH 15/91] Add __call__ for sphericaldf --- galpy/df/sphericaldf.py | 85 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index afddd75bf..5b557a59b 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -44,11 +44,94 @@ def __init__(self,pot=None,ro=None,vo=None): self._pot = flatten_potential(pot) ############################## EVALUATING THE DF############################### + @physical_conversion('phasespacedensity',pop=True) def __call__(self,*args,**kwargs): + """ + NAME: + + __call__ + + PURPOSE: + + return the DF + + INPUT: + + Either: + + a) (E,L,Lz): tuple of E and (optionally) L and (optionally) Lz. + Each may be Quantity + + b) R,vR,vT,z,vz,phi: + + c) Orbit instance: orbit.Orbit instance and if specific time + then orbit.Orbit(t) + + KWARGS: + + return_fE= if True then return the full distribution function plus + just the energy component (for e.g. an anisotropic DF) + + OUTPUT: + + Value of DF + + HISTORY: + + 2020-07-22 - Written - + + """ # Stub for calling the DF as a function of either a) R,vR,vT,z,vz,phi, # b) Orbit, c) E,L (Lz?) --> maybe depends on the actual form? - return None + # Get E,L,Lz. Generic requirements for any possible spherical DF? + if len(args) == 1: + if not isinstance(args[0],Orbit): # Assume tuple (E,L,Lz) + if len(args[0]) == 1: + E = args[0][0] + L,Lz = None,None + elif len(args[0]) == 2: + E,L = args[0] + Lz = None + elif len(args[0]) == 3: + E,L,Lz = args[0] + else: # Orbit + E = args[0].E(pot=self._pot) + L = numpy.sqrt(numpy.sum(numpy.square(args[0].L()))) + Lz = args[0].Lz() + if _APY_LOADED and isinstance(E,units.Quantity): + E.to(units.km**2/units.s**2).value/self._vo**2. + if _APY_LOADED and isinstance(L,units.Quantity): + L.to(units.kpc*units.km/units.s).value/self._ro/self._vo + if _APY_LOADED and isinstance(Lz,units.Quantity): + Lz.to(units.kpc*units.km/units.s).value/self._ro/self._vo + else: # Assume R,vR,vT,z,vz,(phi) + if len(args) == 5: + R,vR,vT,z,vz = args + phi = None + else: + R,vR,vT,z,vz,phi = args + if _APY_LOADED and isinstance(R,units.Quantity): + R.to(units.kpc).value/self._ro + if _APY_LOADED and isinstance(vR,units.Quantity): + vR.to(units.km/units.s).value/self._vo + if _APY_LOADED and isinstance(vT,units.Quantity): + vT.to(units.km/units.s).value/self._vo + if _APY_LOADED and isinstance(z,units.Quantity): + z.to(units.kpc).value/self._ro + if _APY_LOADED and isinstance(vz,units.Quantity): + vz.to(units.km/units.s).value/self._vo + if _APY_LOADED and isinstance(phi,units.Quantity): + phi.to(units.rad).value + vtotSq = vR**2.+vT**2.+vz**2. + E = 0.5*vtotSq + evaluatePotentials(R,z) + Lz = R*vT + r = numpy.sqrt(R**2.+z**2.) + vrad = (R*vR+z*vz)/r + L = numpy.sqrt(vtotSq-vrad**2.)*r + f = self.__call_internal__(E,L,Lz) # Some function for each sub-class + return f + ############################### SAMPLING THE DF################################ def sample(self): # Stub for main sampling function, which will return (x,v) or Orbits... From cdf125af9a39035042e820331f570e5e37655a16 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:39:35 -0700 Subject: [PATCH 16/91] Add top-level sample function for sphericaldf --- galpy/df/sphericaldf.py | 72 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 5b557a59b..ca69ae244 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -133,9 +133,75 @@ def __call__(self,*args,**kwargs): return f ############################### SAMPLING THE DF################################ - def sample(self): - # Stub for main sampling function, which will return (x,v) or Orbits... - return None + def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): + """ + NAME: + + sample + + PURPOSE: + + Return full 6D samples of the DF + + INPUT: + + R= Radius at which to generate samples (can be Quantity) + + z= Height at which to generate samples (can be Quantity) + + phi= Azimuth at which to generate samples (can be Quantity) + + n= number of samples to generate + + OPTIONAL INPUT: + + return_orbit= If True output is orbit.Orbit object, if False + output is (R,vR,vT,z,vz,phi) + + OUTPUT: + + List of samples. Either vector (R,vR,vT,z,vz,phi) or orbit.Orbit + + NOTES: + + If R,z,phi are None then sample positions with CMF. If R,z,phi are + floats then sample n velocities at location. If array then sample + velocities at radii, ignoring n. phi can be None if R,z are set + by any above mechanism, will then sample phi for output. + + HISTORY: + + 2020-07-22 - Written - + + """ + if R is None and z is None: # Full 6D samples + r = self._sample_r(n=n) + v = self._sample_v(r,n=n) + phi,theta = self._sample_position_angles(n=n) + R = r*numpy.sin(theta) + z = r*numpy.cos(theta) + else: # 3D velocity samples + if isinstance(R,numpy.ndarray): + assert len(R) == len(z) + n = len(R) + r = numpy.sqrt(R**2.+z**2.) + v = self._sample_v(r,n=n) + if phi is None: # Otherwise assume phi input type matches R,z + phi,_ = self._sample_position_angles(n=n) + + eta,psi = self._sample_velocity_angles(n=n) + vr = v*numpy.cos(eta) + vtheta = v*numpy.sin(eta)*numpy.cos(psi) + vT = v*numpy.sin(eta)*numpy.sin(psi) + vR_samples = vr*numpy.sin(theta) + vtheta*numpy.cos(theta) + vz_samples = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) + + if return_orbit: + o = Orbit(vxvv=numpy.array([R,vR,vT,z,vz,phi]).T, + ro=self._ro,vo=self._vo) + return o + else: + return (R,vR,vT,z,vz,phi) def _sample_r(self,n=1): # Stub for sampling the radius from M( Date: Thu, 23 Jul 2020 12:40:15 -0700 Subject: [PATCH 17/91] Add position sampling functions --- galpy/df/sphericaldf.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index ca69ae244..d491e7710 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -204,13 +204,23 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): return (R,vR,vT,z,vz,phi) def _sample_r(self,n=1): - # Stub for sampling the radius from M( Date: Thu, 23 Jul 2020 12:41:06 -0700 Subject: [PATCH 18/91] Add top-level velocity sampling functions --- galpy/df/sphericaldf.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index d491e7710..6a655d073 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -222,15 +222,15 @@ def _sample_position_angles(self,n=1): return phi_samples,theta_samples def _sample_v(self,r,n=1): - # Stub for sampling the magnitude of the velocity at a given r - # Uses methods for defining how the mag of the velocity is sampled - # defined in subclasses - return None - - def _sample_velocity_angles(self,r,n=1): - # Stub for sampling the angles eta and psi for the velocities - # Uses _sample_eta implemented in subclasses - return None + """Generate velocity samples""" + v_samples = self._sample_v_internal(r,n=n) # Different for each type of DF + return v_samples + + def _sample_velocity_angles(self,n=1): + """Generate samples of angles that set radial vs tangential velocities""" + eta_samples = self._sample_eta(n) + psi_samples = numpy.random.uniform(size=n)*2*numpy.pi + return eta_samples,psi_samples class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" From a030ae987ce302c5e9cc27d6ca47f307bec93b18 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 23 Jul 2020 12:41:31 -0700 Subject: [PATCH 19/91] Add __init__ for anisotropicsphericaldf --- galpy/df/sphericaldf.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 6a655d073..4f7f225af 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -234,5 +234,31 @@ def _sample_velocity_angles(self,n=1): class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" - def __init__(self,ro=None,vo=None): - sphericaldf.__init__(self,ro=ro,vo=vo) \ No newline at end of file + def __init__(self,pot=None,dftype=None,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize an anisotropic distribution function + + INPUT: + + dftype= Type of anisotropic DF, either 'constant' for constant beta + over all r, or 'ossipkov-merrit' + + pot - Spherical potential which determines the DF + + OUTPUT: + + None + + HISTORY: + + 2020-07-22 - Written - + + """ + sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) + self._dftype = dftype \ No newline at end of file From f137db168171d387753bdbd0f9329302e0a482b2 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 27 Jul 2020 09:34:59 -0700 Subject: [PATCH 20/91] Add functions for sampling eta --- galpy/df/sphericaldf.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 4f7f225af..9ba38af83 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -231,6 +231,29 @@ def _sample_velocity_angles(self,n=1): eta_samples = self._sample_eta(n) psi_samples = numpy.random.uniform(size=n)*2*numpy.pi return eta_samples,psi_samples + + def _sample_eta(self,n=1): + """Sample the angle eta""" + deta = 0.00005*numpy.pi + etas = (np.arange(0, np.pi, deta)+deta/2) + if hasattr(self,'beta'): + eta_pdf_cml = numpy.cumsum(self.eta_pdf(etas,self.beta)) + else: + eta_pdf_cml = numpy.cumsum(self.eta_pdf(etas,0)) + eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] + eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, + bounds_error=False, fill_value='extrapolate') + eta_samples = eta_icml_interp(np.random.uniform(size=n)) + + def _eta_pdf(self,eta,beta,norm=True): + """PDF for sampling eta""" + p_eta = np.sin( eta )**(1.-2.*beta) + if norm: + p_eta /= numpy.sqrt(np.pi)\ + *scipy.special.gamma(1-self.beta)\ + /scipy.special.gamma(1.5-self.beta) + return p_eta + class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" From e9d880d109bddfc20fa2c540a459dbc6a378db43 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 27 Jul 2020 10:29:47 -0700 Subject: [PATCH 21/91] Set f1=0 for out-of-bounds energies --- galpy/df/constantbetaHernquistdf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index fd933a5a5..a40544145 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -99,7 +99,7 @@ def f1E(self,E): # Handle potential E outside of bounds Etilde_out = numpy.where(Etilde<0|Etilde>1)[0] if len(Etilde_out)>0: - # Set to dummy and NaN later, wierd but prevents functions throwing errors + # Set to dummy and 0 later, wierd but prevents functions throwing errors Etilde[Etilde_out]=0.5 # Evaluate depending on beta @@ -118,8 +118,8 @@ def f1E(self,E): f1 = self._f1_beta_gt05_Hernquist(Erel) else: f1 = self._f1_any_beta(Erel) # This function sits in the super class? - - + if len(Etilde_out)>0: + f1[Etilde_out] = 0 return f1 def _f1_beta_gt05_Hernquist(self,Erel): From 3696c67c5c22e0385b0afe83c64aa3940f777dca Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:21:48 -0700 Subject: [PATCH 22/91] Make _scale attribute from potential --- galpy/df/sphericaldf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 9ba38af83..8eb205c3a 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -14,7 +14,7 @@ class sphericaldf(df): """Superclass for spherical distribution functions""" - def __init__(self,pot=None,ro=None,vo=None): + def __init__(self,pot=None,scale=None,ro=None,vo=None): """ NAME: @@ -28,6 +28,10 @@ def __init__(self,pot=None,ro=None,vo=None): pot - Spherical potential which determines the DF + scale - Characteristic scale radius to aid sampling calculations. + Not necessary, and will also be overridden by value from pot if + available. + OUTPUT: None From 17a42a5a1899662a0651d4a928bdc74a0aaca7d0 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:23:33 -0700 Subject: [PATCH 23/91] Add _scale, single pot check, cmf_interpolator to __init__ --- galpy/df/sphericaldf.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 8eb205c3a..58a781ccd 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -6,7 +6,7 @@ import scipy.interpolate from .df import df, _APY_LOADED from ..potential import flatten as flatten_potential -from ..potential import evaluatePotentials +from ..potential import evaluatePotentials, vesc from ..orbit import Orbit from ..util.bovy_conversion import physical_conversion if _APY_LOADED: @@ -45,7 +45,19 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): if pot is None: raise IOError("pot= must be set") # Some sort of check for spherical symmetry in the potential? - self._pot = flatten_potential(pot) + assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' + self._pot = pot + self._potInf = evaluatePotentials(pot,10**12,0) + try: + self._scale = pot._scale + except AttributeError: + if scale is not None: + if _APY_LOADED and isinstance(scale,units.Quantity): + scale= scale.to(u.kpc).value/self._ro + self._scale = scale + else: + self._scale = 1. + self._xi_cmf_interpolator = self._make_cmf_interpolator() ############################## EVALUATING THE DF############################### @physical_conversion('phasespacedensity',pop=True) From c6de6852174dcd881e15c5ca8c49af6a68d3c8e8 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:27:04 -0700 Subject: [PATCH 24/91] Fix astropy units .to syntax --- galpy/df/constantbetaHernquistdf.py | 2 +- galpy/df/isotropicHernquistdf.py | 2 +- galpy/df/sphericaldf.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index a40544145..da27efe93 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -91,9 +91,9 @@ def f1E(self,E): 2020-07-22 - Written """ if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E.to(units.km**2/units.s**2).value/vo**2. # Scale energies phi0 = evaluatePotentials(self._pot,0,0) + E= E.to(units.km**2/units.s**2).value/self._vo**2. Erel = -E Etilde = Erel/phi0 # Handle potential E outside of bounds diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index aba37e999..8c0f69cf4 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -36,7 +36,7 @@ def __call_internal__(self,*args): """ E = args[0] if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E.to(units.km**2/units.s**2).value/vo**2. + E= E.to(units.km**2/units.s**2).value/vo**2. # Scale energies phi0 = evaluatePotentials(self._pot,0,0) Erel = -E diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 58a781ccd..d72ebb6a8 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -116,11 +116,11 @@ def __call__(self,*args,**kwargs): L = numpy.sqrt(numpy.sum(numpy.square(args[0].L()))) Lz = args[0].Lz() if _APY_LOADED and isinstance(E,units.Quantity): - E.to(units.km**2/units.s**2).value/self._vo**2. + E= E.to(units.km**2/units.s**2).value/self._vo**2. if _APY_LOADED and isinstance(L,units.Quantity): - L.to(units.kpc*units.km/units.s).value/self._ro/self._vo + L= L.to(units.kpc*units.km/units.s).value/self._ro/self._vo if _APY_LOADED and isinstance(Lz,units.Quantity): - Lz.to(units.kpc*units.km/units.s).value/self._ro/self._vo + Lz= Lz.to(units.kpc*units.km/units.s).value/self._ro/self._vo else: # Assume R,vR,vT,z,vz,(phi) if len(args) == 5: R,vR,vT,z,vz = args @@ -128,17 +128,17 @@ def __call__(self,*args,**kwargs): else: R,vR,vT,z,vz,phi = args if _APY_LOADED and isinstance(R,units.Quantity): - R.to(units.kpc).value/self._ro + R= R.to(units.kpc).value/self._ro if _APY_LOADED and isinstance(vR,units.Quantity): - vR.to(units.km/units.s).value/self._vo + vR= vR.to(units.km/units.s).value/self._vo if _APY_LOADED and isinstance(vT,units.Quantity): - vT.to(units.km/units.s).value/self._vo + vT= vT.to(units.km/units.s).value/self._vo if _APY_LOADED and isinstance(z,units.Quantity): - z.to(units.kpc).value/self._ro + z= z.to(units.kpc).value/self._ro if _APY_LOADED and isinstance(vz,units.Quantity): - vz.to(units.km/units.s).value/self._vo + vz= vz.to(units.km/units.s).value/self._vo if _APY_LOADED and isinstance(phi,units.Quantity): - phi.to(units.rad).value + phi= phi.to(units.rad).value vtotSq = vR**2.+vT**2.+vz**2. E = 0.5*vtotSq + evaluatePotentials(R,z) Lz = R*vT From a8b983dd78423412a728910d0d064e749b3e5bec Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:27:55 -0700 Subject: [PATCH 25/91] Change position sampling to xi formalism --- galpy/df/sphericaldf.py | 51 +++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index d72ebb6a8..d9bef210b 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -218,25 +218,52 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): return o else: return (R,vR,vT,z,vz,phi) - + def _sample_r(self,n=1): - """Generate radial position samples from potential""" - # Maybe interpolator initialization can happen in separate function - # to be called in __init__. It is fast though. - r_grid = numpy.logspace(-3,3) - cml_mass_frac_grid = self._pot.mass(r_grid)/self._pot.mass(r_grid[-1]) - icml_mass_frac_interp = scipy.interpolate.interp1d(cml_mass_frac_grid, - r_grid,kind='cubic',bounds_error=False,fill_value='extrapolate') - rand_mass_frac = numpy.random.uniform(size=n) - r_ramples = icml_mass_frac_interp(rand_mass_frac) - return r_samples + """Generate radial position samples from potential + Note - the function interpolates the normalized CMF onto the variable + xi defined as: + + .. math:: \\xi = \\frac{r-1}{r+1} + + so that xi is in the range [-1,1], which corresponds to an r range of + [0,infinity)""" + rand_mass_frac = numpy.random.random(size=n) + xi_samples = self._xi_cmf_interpolator(rand_mass_frac) + return self._xi_to_r(xi_samples,a=self._scale) + + def _make_cmf_interpolator(self): + """Create the interpolator object for calculating radii from the CMF + Note - must use self.xi_to_r() on any output of interpolator + Note - the function interpolates the normalized CMF onto the variable + xi defined as: + + .. math:: \\xi = \\frac{r-1}{r+1} + + so that xi is in the range [-1,1], which corresponds to an r range of + [0,infinity)""" + xis = numpy.arange(-1,1,1e-6) + rs = self._xi_to_r(xis,a=self._scale) + ms = self._pot.mass(rs,use_physical=False) + ms /= self._pot.mass(10**12,use_physical=False) + xis_cmf_interp = scipy.interpolate.interp1d(ms,xis, + kind='cubic',bounds_error=False,fill_value='extrapolate') + return xis_cmf_interp + + def _xi_to_r(self,xi,a=1): + """Calculate r from xi""" + return a*numpy.divide(1+xi,1-xi) + + def r_to_xi(self,r,a=1): + """Calculate xi from r""" + return numpy.divide(r/a-1,r/a+1) def _sample_position_angles(self,n=1): """Generate spherical angle samples""" phi_samples = numpy.random.uniform(size=n)*2*numpy.pi theta_samples = numpy.arccos(2*numpy.random.uniform(size=n)-1) return phi_samples,theta_samples - + def _sample_v(self,r,n=1): """Generate velocity samples""" v_samples = self._sample_v_internal(r,n=n) # Different for each type of DF From ffcaa28c23d8d08e5d5f48b40604fa4230a158b8 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:28:20 -0700 Subject: [PATCH 26/91] Implements simplified velocity sampling --- galpy/df/sphericaldf.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index d9bef210b..48df2671e 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -266,15 +266,19 @@ def _sample_position_angles(self,n=1): def _sample_v(self,r,n=1): """Generate velocity samples""" - v_samples = self._sample_v_internal(r,n=n) # Different for each type of DF - return v_samples + assert hasattr(self,'_v_vesc_icdf_interpolator') + vesc_vals = vesc(self._pot,r,use_physical=False) + icdf_samples = numpy.random.random(size=n) + v_vesc_samples = self._v_vesc_icdf_interpolator(numpy.log10(r/self._scale), + icdf_samples) + return numpy.diag(v_vesc_samples)*vesc_vals def _sample_velocity_angles(self,n=1): """Generate samples of angles that set radial vs tangential velocities""" eta_samples = self._sample_eta(n) psi_samples = numpy.random.uniform(size=n)*2*numpy.pi return eta_samples,psi_samples - + def _sample_eta(self,n=1): """Sample the angle eta""" deta = 0.00005*numpy.pi From e6542255bd7d90b98ac01e34bd990d8154d69b0b Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:28:48 -0700 Subject: [PATCH 27/91] Minor fixes to eta sampling functions --- galpy/df/sphericaldf.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 48df2671e..7731acef5 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -282,21 +282,22 @@ def _sample_velocity_angles(self,n=1): def _sample_eta(self,n=1): """Sample the angle eta""" deta = 0.00005*numpy.pi - etas = (np.arange(0, np.pi, deta)+deta/2) + etas = (numpy.arange(0, numpy.pi, deta)+deta/2) if hasattr(self,'beta'): - eta_pdf_cml = numpy.cumsum(self.eta_pdf(etas,self.beta)) + eta_pdf_cml = numpy.cumsum(self._eta_pdf(etas,self.beta)) else: - eta_pdf_cml = numpy.cumsum(self.eta_pdf(etas,0)) + eta_pdf_cml = numpy.cumsum(self._eta_pdf(etas,0)) eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, bounds_error=False, fill_value='extrapolate') - eta_samples = eta_icml_interp(np.random.uniform(size=n)) - + eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) + return eta_samples + def _eta_pdf(self,eta,beta,norm=True): """PDF for sampling eta""" - p_eta = np.sin( eta )**(1.-2.*beta) + p_eta = numpy.sin( eta )**(1.-2.*beta) if norm: - p_eta /= numpy.sqrt(np.pi)\ + p_eta /= numpy.sqrt(numpy.pi)\ *scipy.special.gamma(1-self.beta)\ /scipy.special.gamma(1.5-self.beta) return p_eta From 3dbcd5fe069f3c513322a003d3207a809f611f5f Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:29:33 -0700 Subject: [PATCH 28/91] Adds function to calculate velocity sampling grid --- galpy/df/sphericaldf.py | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 7731acef5..cbb9ea3da 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -301,7 +301,84 @@ def _eta_pdf(self,eta,beta,norm=True): *scipy.special.gamma(1-self.beta)\ /scipy.special.gamma(1.5-self.beta) return p_eta + + def calculate_velocity_sampling_grid(self, r_a_start=-3, r_a_end=3, + r_a_interval=0.05, v_vesc_interval=0.01, set_interpolator=True, + output_grid=False): + ''' + NAME: + + calculate_velocity_sampling_grid + + PURPOSE: + + Calculate a grid of the velocity sampling function v^2*f(E) over many + radii. The radii are fractional with respect to some scale radius + which characteristically describes the size of the potential, + and the velocities are fractional with respect to the escape velocity + at each radius r. This information is saved in a 2D interpolator which + represents the inverse cumulative distribution at many radii. This + allows for sampling of v/vesc given an input r/a + + INPUT: + + r_a_start= radius grid start location in units of r/a + + r_a_end= radius grid end location in units of r/a + + r_a_interval= radius grid spacing in units of r/a + + v_vesc_interval= velocity grid spacing in units of v/vesc + + OUTPUT: + + None (But sets self._v_vesc_icdf_interpolator) + + HISTORY: + + Written 2020-07-24 - James Lane (UofT) + ''' + # Make an array of r/a by v/vesc and then orbits to calculate fE + r_a_values = numpy.power(10,numpy.arange(r_a_start,r_a_end,r_a_interval)) + v_vesc_values = numpy.arange(0,1,v_vesc_interval) + r_a_grid, v_vesc_grid = numpy.meshgrid(r_a_values,v_vesc_values) + vesc_grid = vesc(self._pot,r_a_grid*self._scale,use_physical=False) + E_grid = evaluatePotentials(self._pot,r_a_grid*self._scale,0, + use_physical=False)+0.5*(numpy.multiply(v_vesc_grid,vesc_grid))**2. + + # Calculate cumulative p(v|r) + fE_grid = self.fE(E_grid).reshape(E_grid.shape) + _beta = 0 + if hasattr(self,'beta'): + _beta = self.beta + pvr_grid = numpy.multiply(fE_grid,(v_vesc_grid*vesc_grid)**(2-2*_beta)) + pvr_grid_cml = numpy.cumsum( pvr_grid, axis=0 ) + pvr_grid_cml_norm = pvr_grid_cml\ + /numpy.repeat(pvr_grid_cml[-1,:][:,numpy.newaxis],pvr_grid_cml.shape[0],axis=1).T + + # Construct the inverse cumulative distribution + n_new_pvr = 100 # Must be multiple of r_a_grid.shape[0] + icdf_pvr_grid_reg = numpy.zeros((n_new_pvr,len(r_a_values))) + icdf_v_vesc_grid_reg = numpy.zeros((n_new_pvr,len(r_a_values))) + r_a_grid_reg = numpy.repeat(r_a_grid,n_new_pvr/r_a_grid.shape[0],axis=0) + for i in range(pvr_grid_cml_norm.shape[1]): + cml_pvr = pvr_grid_cml_norm[:,i] + cml_pvr_inv_interp = scipy.interpolate.interp1d(cml_pvr, + v_vesc_values, kind='cubic', bounds_error=None, + fill_value='extrapolate') + pvr_samples_reg = numpy.linspace(0,1,num=n_new_pvr,endpoint=False) + v_vesc_samples_reg = cml_pvr_inv_interp(pvr_samples_reg) + icdf_pvr_grid_reg[:,i] = pvr_samples_reg + icdf_v_vesc_grid_reg[:,i] = v_vesc_samples_reg + ###i + # Create the interpolator + self._r_a_values = r_a_values + self._v_vesc_icdf_interpolator = scipy.interpolate.interp2d( + numpy.log10(r_a_grid_reg.flatten()), icdf_pvr_grid_reg.flatten(), + icdf_v_vesc_grid_reg.flatten(), kind='cubic', + bounds_error=False, fill_value=None) + return None class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" From 5a25cbd0689ddd301b60ed03674e594698e15b75 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:31:09 -0700 Subject: [PATCH 29/91] Fix sample function syntax --- galpy/df/sphericaldf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index cbb9ea3da..debfcb28d 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -190,7 +190,7 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): 2020-07-22 - Written - """ - if R is None and z is None: # Full 6D samples + if R is None or z is None: # Full 6D samples r = self._sample_r(n=n) v = self._sample_v(r,n=n) phi,theta = self._sample_position_angles(n=n) @@ -209,8 +209,8 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): vr = v*numpy.cos(eta) vtheta = v*numpy.sin(eta)*numpy.cos(psi) vT = v*numpy.sin(eta)*numpy.sin(psi) - vR_samples = vr*numpy.sin(theta) + vtheta*numpy.cos(theta) - vz_samples = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) + vR = vr*numpy.sin(theta) + vtheta*numpy.cos(theta) + vz = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) if return_orbit: o = Orbit(vxvv=numpy.array([R,vR,vT,z,vz,phi]).T, From 1d27d4165759848b266ac40b680e0f2c322d8207 Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:32:11 -0700 Subject: [PATCH 30/91] Change f1 to fE and standardize across files --- galpy/df/constantbetaHernquistdf.py | 10 +++++----- galpy/df/constantbetadf.py | 2 +- galpy/df/isotropicHernquistdf.py | 27 +++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index da27efe93..14a7b7360 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -65,14 +65,14 @@ def __call_internal__(self,*args): """ E = args[0] L = args[1] - f1 = self.f1E(E) - return L**(-2*self.beta)*f1 + fE = self.fE(E) + return L**(-2*self.beta)*fE - def f1E(self,E): + def fE(self,E): """ NAME: - f1E + fE PURPOSE @@ -84,7 +84,7 @@ def f1E(self,E): OUTPUT: - f1E - The value of the energy portion of the DF + fE - The value of the energy portion of the DF HISTORY: diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 38b1799b2..2ca4e81d3 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -22,6 +22,6 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): ro=ro,vo=vo) self.beta = beta - def f1E(self,E): + def fE(self,E): # Stub for computing f_1(E) in BT08 nomenclature return None diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index 8c0f69cf4..ed5d512fe 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -56,5 +56,28 @@ def __call_internal__(self,*args): if len(Etilde_out) > 0: fH[Etilde_out] = 0 return fH - - + + def fE(self,E): + """ + NAME: + + fE + + PURPOSE + + Calculate the energy portion of an isotropic Hernquist distribution + function + + INPUT: + + E - The energy (can be Quantity) + + OUTPUT: + + fE - The value of the energy portion of the DF + + HISTORY: + + 2020-08-09 - Written - James Lane (UofT) + """ + return self.__call_internal__(E) From 309b29df1d8e13367b5531e6bc831d1c6142466e Mon Sep 17 00:00:00 2001 From: James Lane Date: Mon, 10 Aug 2020 10:32:35 -0700 Subject: [PATCH 31/91] Syntax fixes to __call_internal --- galpy/df/constantbetaHernquistdf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 14a7b7360..f5e52a575 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -91,24 +91,24 @@ def fE(self,E): 2020-07-22 - Written """ if _APY_LOADED and isinstance(E,units.quantity.Quantity): - # Scale energies - phi0 = evaluatePotentials(self._pot,0,0) E= E.to(units.km**2/units.s**2).value/self._vo**2. + psi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) Erel = -E - Etilde = Erel/phi0 + Etilde = Erel/psi0 # Handle potential E outside of bounds - Etilde_out = numpy.where(Etilde<0|Etilde>1)[0] + Etilde_out = numpy.where(numpy.logical_or(Etilde<0,Etilde>1))[0] if len(Etilde_out)>0: - # Set to dummy and 0 later, wierd but prevents functions throwing errors + # Dummy variable now and 0 later, prevents numerical issues? Etilde[Etilde_out]=0.5 # Evaluate depending on beta - _GMa = phi0*self._pot.a**2. + _GMa = psi0*self._pot.a**2. if self.beta == 0.: f1 = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ - +((3*numpy.arcsin(numpy.sqrt(Etilde)))/numpy.sqrt(Etilde*(1-Etilde)))) + +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ + /numpy.sqrt(Etilde*(1-Etilde)))) elif self.beta == 0.5: f1 = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) elif self.beta == -0.5: From bf0d6965ea72ff3a39d138d80d09511dfd3b838c Mon Sep 17 00:00:00 2001 From: James Lane Date: Wed, 12 Aug 2020 15:41:53 -0700 Subject: [PATCH 32/91] Add optional analytic inverse cumulative mass function for speed --- galpy/df/constantbetaHernquistdf.py | 5 +++++ galpy/df/isotropicHernquistdf.py | 5 +++++ galpy/df/sphericaldf.py | 10 ++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index f5e52a575..ef080f37d 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -147,3 +147,8 @@ def _f1_beta_gt05_integral_Hernquist(self,psi,Erel,psi0): *numpy.power(1-psiTilde,2*self.beta-2)\ *numpy.power(psiTilde,4-2*self.beta)) return numer/denom + + def _icmf(self,ms): + '''Analytic expression for the normalized inverse cumulative mass + function. The argument ms is normalized mass fraction [0,1]''' + return self._pot.a*numpy.sqrt(ms)/(1-numpy.sqrt(ms)) diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index ed5d512fe..ad8457b40 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -81,3 +81,8 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ return self.__call_internal__(E) + + def _icmf(self,ms): + '''Analytic expression for the normalized inverse cumulative mass + function. The argument ms is normalized mass fraction [0,1]''' + return self._pot.a*numpy.sqrt(ms)/(1-numpy.sqrt(ms)) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index debfcb28d..e06802ebf 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -229,8 +229,12 @@ def _sample_r(self,n=1): so that xi is in the range [-1,1], which corresponds to an r range of [0,infinity)""" rand_mass_frac = numpy.random.random(size=n) - xi_samples = self._xi_cmf_interpolator(rand_mass_frac) - return self._xi_to_r(xi_samples,a=self._scale) + if '_icmf' in dir(self): + r_samples = self._icmf(rand_mass_frac) + else: + xi_samples = self._xi_cmf_interpolator(rand_mass_frac) + r_samples = self._xi_to_r(xi_samples,a=self._scale) + return r_samples def _make_cmf_interpolator(self): """Create the interpolator object for calculating radii from the CMF @@ -246,6 +250,8 @@ def _make_cmf_interpolator(self): rs = self._xi_to_r(xis,a=self._scale) ms = self._pot.mass(rs,use_physical=False) ms /= self._pot.mass(10**12,use_physical=False) + xis = numpy.append(xis,1) + ms = numpy.append(ms,1) xis_cmf_interp = scipy.interpolate.interp1d(ms,xis, kind='cubic',bounds_error=False,fill_value='extrapolate') return xis_cmf_interp From 0afa31f0deac6acada8b1510555aa741668e6db5 Mon Sep 17 00:00:00 2001 From: James Lane Date: Wed, 12 Aug 2020 15:43:55 -0700 Subject: [PATCH 33/91] Change integrator to loop over grid args, formalize fE naming --- galpy/df/constantbetaHernquistdf.py | 22 +++++++++++++--------- galpy/df/constantbetadf.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index ef080f37d..2d2c8aabb 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -115,16 +115,16 @@ def fE(self,E): f1 = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) elif self.beta < 1.0 and self.beta > 0.5: - f1 = self._f1_beta_gt05_Hernquist(Erel) + f1 = self._fE_beta_gt05(Erel) else: - f1 = self._f1_any_beta(Erel) # This function sits in the super class? + f1 = self._fE_any_beta(Erel) # This function sits in the super class? if len(Etilde_out)>0: f1[Etilde_out] = 0 return f1 - def _f1_beta_gt05_Hernquist(self,Erel): - """Calculate f1 for a Hernquist model when 0.5 < beta < 1.0""" - psi0 = evaluatePotentials(self._pot,0,0) + def _fE_beta_gt05(self,Erel): + """Calculate fE for a Hernquist model when 0.5 < beta < 1.0""" + psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) _a = self._pot.a _GM = psi0*_a Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self.beta)\ @@ -133,12 +133,16 @@ def _f1_beta_gt05_Hernquist(self,Erel): alpha = self.beta-0.5 coeff = (Cbeta*_a**(2*self.beta-2))*(numpy.sin(alpha*numpy.pi))\ /(_GM*2*numpy.pi**2) - integral = scipy.integrate.quad(self.f1_beta_gt05_integral, - a=0, b=Erel, args=(Erel,psi0) )[0] + integral = numpy.zeros_like(Erel) + for ii in range(Erel.shape[0]): + for jj in range(Erel.shape[1]): + integral[ii,jj] = scipy.integrate.quad( + self._fE_beta_gt05_integral, a=0, b=Erel[ii,jj], + args=(Erel[ii,jj],psi0) )[0] return coeff*integral - def _f1_beta_gt05_integral_Hernquist(self,psi,Erel,psi0): - """Integral for calculating f1 for a Hernquist when 0.5 < beta < 1.0""" + def _fE_beta_gt05_integral(self,psi,Erel,psi0): + """Integral for calculating fE for a Hernquist when 0.5 < beta < 1.0""" psiTilde = psi/psi0 # Absolute value because the answer normally comes out imaginary? denom = numpy.abs( (Erel-psi)**(1.5-self.beta) ) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 2ca4e81d3..09626e1a1 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -18,9 +18,9 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): pot - Spherical potential which determines the DF """ + self.beta = beta anisotropicsphericaldf.__init__(self,pot=pot,dftype='constant', ro=ro,vo=vo) - self.beta = beta def fE(self,E): # Stub for computing f_1(E) in BT08 nomenclature From a0ce9bf03d4187c655881ba533e3ec2268504358 Mon Sep 17 00:00:00 2001 From: James Lane Date: Wed, 12 Aug 2020 15:44:47 -0700 Subject: [PATCH 34/91] Updates P(v|r) interpolator to grid for speed. Formalize variable naming --- galpy/df/sphericaldf.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index e06802ebf..b1ec2d5ca 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -58,6 +58,7 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): else: self._scale = 1. self._xi_cmf_interpolator = self._make_cmf_interpolator() + self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() ############################## EVALUATING THE DF############################### @physical_conversion('phasespacedensity',pop=True) @@ -272,11 +273,10 @@ def _sample_position_angles(self,n=1): def _sample_v(self,r,n=1): """Generate velocity samples""" - assert hasattr(self,'_v_vesc_icdf_interpolator') vesc_vals = vesc(self._pot,r,use_physical=False) - icdf_samples = numpy.random.random(size=n) - v_vesc_samples = self._v_vesc_icdf_interpolator(numpy.log10(r/self._scale), - icdf_samples) + pvr_icdf_samples = numpy.random.random(size=n) + v_vesc_samples = self._v_vesc_pvr_interpolator(numpy.log10(r/self._scale), + pvr_icdf_samples) return numpy.diag(v_vesc_samples)*vesc_vals def _sample_velocity_angles(self,n=1): @@ -308,13 +308,13 @@ def _eta_pdf(self,eta,beta,norm=True): /scipy.special.gamma(1.5-self.beta) return p_eta - def calculate_velocity_sampling_grid(self, r_a_start=-3, r_a_end=3, + def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, r_a_interval=0.05, v_vesc_interval=0.01, set_interpolator=True, output_grid=False): ''' NAME: - calculate_velocity_sampling_grid + _make_pvr_interpolator PURPOSE: @@ -338,7 +338,7 @@ def calculate_velocity_sampling_grid(self, r_a_start=-3, r_a_end=3, OUTPUT: - None (But sets self._v_vesc_icdf_interpolator) + None (But sets self._v_vesc_pvr_interpolator) HISTORY: @@ -379,12 +379,10 @@ def calculate_velocity_sampling_grid(self, r_a_start=-3, r_a_end=3, ###i # Create the interpolator - self._r_a_values = r_a_values - self._v_vesc_icdf_interpolator = scipy.interpolate.interp2d( - numpy.log10(r_a_grid_reg.flatten()), icdf_pvr_grid_reg.flatten(), - icdf_v_vesc_grid_reg.flatten(), kind='cubic', - bounds_error=False, fill_value=None) - return None + v_vesc_icdf_interpolator = scipy.interpolate.interp2d( + numpy.log10(r_a_grid[0,:]), icdf_pvr_grid_reg[:,0], + icdf_v_vesc_grid_reg, bounds_error=False,fill_value=None) + return v_vesc_icdf_interpolator class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" @@ -414,5 +412,5 @@ def __init__(self,pot=None,dftype=None,ro=None,vo=None): 2020-07-22 - Written - """ - sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) - self._dftype = dftype \ No newline at end of file + self._dftype = dftype + sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) \ No newline at end of file From f3fc974541db18db05ed13809f193eea27344fc1 Mon Sep 17 00:00:00 2001 From: James Lane Date: Thu, 13 Aug 2020 15:02:48 -0700 Subject: [PATCH 35/91] Add -0.5 < beta < 0.5 Hernquist analytic integral solution --- galpy/df/constantbetaHernquistdf.py | 69 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 2d2c8aabb..a78e7b511 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -104,23 +104,25 @@ def fE(self,E): # Evaluate depending on beta _GMa = psi0*self._pot.a**2. if self.beta == 0.: - f1 = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ + fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ /numpy.sqrt(Etilde*(1-Etilde)))) elif self.beta == 0.5: - f1 = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) + fE = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) elif self.beta == -0.5: - f1 = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ + fE = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) elif self.beta < 1.0 and self.beta > 0.5: - f1 = self._fE_beta_gt05(Erel) + fE = self._fE_beta_gt05(Erel) + elif self.beta < 0.5 and self.beta > -0.5: + fE = self._fE_beta_gtm05_lt05(Erel) else: - f1 = self._fE_any_beta(Erel) # This function sits in the super class? + fE = self._fE_any_beta(Erel) # This function sits in the super class? if len(Etilde_out)>0: - f1[Etilde_out] = 0 - return f1 + fE[Etilde_out] = 0 + return fE def _fE_beta_gt05(self,Erel): """Calculate fE for a Hernquist model when 0.5 < beta < 1.0""" @@ -133,12 +135,18 @@ def _fE_beta_gt05(self,Erel): alpha = self.beta-0.5 coeff = (Cbeta*_a**(2*self.beta-2))*(numpy.sin(alpha*numpy.pi))\ /(_GM*2*numpy.pi**2) - integral = numpy.zeros_like(Erel) - for ii in range(Erel.shape[0]): - for jj in range(Erel.shape[1]): - integral[ii,jj] = scipy.integrate.quad( - self._fE_beta_gt05_integral, a=0, b=Erel[ii,jj], - args=(Erel[ii,jj],psi0) )[0] + if hasattr(Erel,'shape'): + _Erel_shape = Erel.shape + _Erel_flat = Erel.flatten() + integral = numpy.zeros_like(_Erel_flat) + for ii in range(len(_Erel_flat)): + integral[ii] = scipy.integrate.quad( + self._fE_beta_gt05_integral, a=0, b=_Erel_flat[ii], + args=(_Erel_flat[ii],psi0))[0] + else: + integral = scipy.integrate.quad( + self._fE_beta_gt05_integral, a=0, b=Erel, + args=(Erel,psi0))[0] return coeff*integral def _fE_beta_gt05_integral(self,psi,Erel,psi0): @@ -152,6 +160,41 @@ def _fE_beta_gt05_integral(self,psi,Erel,psi0): *numpy.power(psiTilde,4-2*self.beta)) return numer/denom + def _fE_beta_gtm05_lt05(self,Erel): + """Calculate fE for a Hernquist model when -0.5 < beta < 0.5""" + psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) + _a = self._pot.a + _GM = psi0*_a + alpha = 0.5-self.beta + Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self.beta)\ + /scipy.special.gamma(1.5-self.beta) + Cbeta = 2**(self.beta-0.5)/(2*numpy.pi*Ibeta*alpha) + coeff = (Cbeta*_a**(2*self.beta-1))*(numpy.sin(alpha*numpy.pi))\ + /((_GM**2)*2*numpy.pi**2) + if hasattr(Erel,'shape'): + _Erel_shape = Erel.shape + _Erel_flat = Erel.flatten() + integral = numpy.zeros_like(_Erel_flat) + for ii in range(len(_Erel_flat)): + integral[ii] = scipy.integrate.quad( + self._fE_beta_gt05_integral, a=0, b=_Erel_flat[ii], + args=(_Erel_flat[ii],psi0))[0] + else: + integral = scipy.integrate.quad( + self._fE_beta_gt05_integral, a=0, b=Erel, + args=(Erel,psi0))[0] + return coeff*integral + + def _fE_beta_gtm05_lt05_integral(self,psi,Erel,psi0): + """Integral for calculating fE for a Hernquist when -0.5 < beta < 0.5""" + psiTilde = psi/psi0 + # Absolute value because the answer normally comes out imaginary? + denom = numpy.abs( (Erel-psi)**(0.5-self.beta) ) + numer = (4-2*self.beta-3*psiTilde)\ + *numpy.power(1-psiTilde,2*self.beta-2)\ + *numpy.power(psiTilde,3-2*self.beta) + return numer/denom + def _icmf(self,ms): '''Analytic expression for the normalized inverse cumulative mass function. The argument ms is normalized mass fraction [0,1]''' From 744bfc63f125cd96e0bd48cb344102a1183a94a7 Mon Sep 17 00:00:00 2001 From: James Lane Date: Tue, 1 Sep 2020 07:56:48 -0700 Subject: [PATCH 36/91] Move eta sampling functions to sub-classes --- galpy/df/Eddingtondf.py | 13 ++++++++++--- galpy/df/constantbetadf.py | 11 +++++++++++ galpy/df/sphericaldf.py | 23 ----------------------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index 3d4ba6cfb..51da94214 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -11,7 +11,14 @@ def fE(self,E): # Stub for computing f(E) return None - def _sample_eta(self): - # Stub for function that samples eta - return None + def _sample_eta(self,n=1): + """Sample the angle eta which defines radial vs tangential velocities""" + deta = 0.00005*numpy.pi + etas = (numpy.arange(0, numpy.pi, deta)+deta/2) + eta_pdf_cml = numpy.cumsum(numpy.sin(etas)) + eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] + eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, + bounds_error=False, fill_value='extrapolate') + eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) + return eta_samples diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 09626e1a1..cb6801428 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -25,3 +25,14 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): def fE(self,E): # Stub for computing f_1(E) in BT08 nomenclature return None + + def _sample_eta(self,n=1): + """Sample the angle eta which defines radial vs tangential velocities""" + deta = 0.00005*numpy.pi + etas = (numpy.arange(0, numpy.pi, deta)+deta/2) + eta_pdf_cml = numpy.cumsum(numpy.power(numpy.sin(etas),1.-2.*self.beta)) + eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] + eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, + bounds_error=False, fill_value='extrapolate') + eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) + return eta_samples diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index b1ec2d5ca..02299b2e5 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -285,29 +285,6 @@ def _sample_velocity_angles(self,n=1): psi_samples = numpy.random.uniform(size=n)*2*numpy.pi return eta_samples,psi_samples - def _sample_eta(self,n=1): - """Sample the angle eta""" - deta = 0.00005*numpy.pi - etas = (numpy.arange(0, numpy.pi, deta)+deta/2) - if hasattr(self,'beta'): - eta_pdf_cml = numpy.cumsum(self._eta_pdf(etas,self.beta)) - else: - eta_pdf_cml = numpy.cumsum(self._eta_pdf(etas,0)) - eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] - eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, - bounds_error=False, fill_value='extrapolate') - eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) - return eta_samples - - def _eta_pdf(self,eta,beta,norm=True): - """PDF for sampling eta""" - p_eta = numpy.sin( eta )**(1.-2.*beta) - if norm: - p_eta /= numpy.sqrt(numpy.pi)\ - *scipy.special.gamma(1-self.beta)\ - /scipy.special.gamma(1.5-self.beta) - return p_eta - def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, r_a_interval=0.05, v_vesc_interval=0.01, set_interpolator=True, output_grid=False): From 53597f8e6b087e60a6c5cd677c464d39bbf9a5fa Mon Sep 17 00:00:00 2001 From: James Lane Date: Tue, 1 Sep 2020 08:00:33 -0700 Subject: [PATCH 37/91] Add Baes & Dejonghe (2002) hypergeometric solution for Hernquist fE --- galpy/df/constantbetaHernquistdf.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index a78e7b511..96166e527 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -12,7 +12,7 @@ class constantbetaHernquistdf(constantbetadf): """Class that implements the anisotropic spherical Hernquist DF with constant beta parameter""" - def __init__(self,pot=None,beta=0,ro=None,vo=None): + def __init__(self,pot=None,beta=0,use_BD02=True,ro=None,vo=None): """ NAME: @@ -26,7 +26,10 @@ def __init__(self,pot=None,beta=0,ro=None,vo=None): pot - Hernquist potential which determines the DF - beta - anisotropy parameter + beta - anisotropy parameter, must be in range [-0.5, 1.0) + + use_BD02 - Use Baes & Dejonghe (2002) solution for f(E) when + non-trivial algebraic solution does not exist OUTPUT: @@ -37,6 +40,8 @@ def __init__(self,pot=None,beta=0,ro=None,vo=None): 2020-07-22 - Written """ assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' + assert -0.5 <= beta and beta < 1.0,'Beta must be in range [-0.5,1.0)' + self._use_BD02 = use_BD02 constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) def __call_internal__(self,*args): @@ -101,7 +106,7 @@ def fE(self,E): # Dummy variable now and 0 later, prevents numerical issues? Etilde[Etilde_out]=0.5 - # Evaluate depending on beta + # First check algebraic solutions _GMa = psi0*self._pot.a**2. if self.beta == 0.: fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ @@ -114,12 +119,12 @@ def fE(self,E): elif self.beta == -0.5: fE = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) + elif self._use_BD02: + fE = self._fE_BD02(Etilde) elif self.beta < 1.0 and self.beta > 0.5: fE = self._fE_beta_gt05(Erel) elif self.beta < 0.5 and self.beta > -0.5: fE = self._fE_beta_gtm05_lt05(Erel) - else: - fE = self._fE_any_beta(Erel) # This function sits in the super class? if len(Etilde_out)>0: fE[Etilde_out] = 0 return fE @@ -194,6 +199,15 @@ def _fE_beta_gtm05_lt05_integral(self,psi,Erel,psi0): *numpy.power(1-psiTilde,2*self.beta-2)\ *numpy.power(psiTilde,3-2*self.beta) return numer/denom + + def _fE_BD02(self,Erel): + """Calculate fE according to the hypergeometric solution of Baes & + Dejonghe (2002)""" + coeff = (2.**self.beta/(2.*numpy.pi)**2.5)*scipy.special.gamma(5.-2.*self.beta)/\ + ( scipy.special.gamma(1.-self.beta)*scipy.special.gamma(3.5-self.beta) ) + fE = coeff*numpy.power(Erel,2.5-self.beta)*\ + scipy.special.hyp2f1(5.-2.*self.beta,1.-2.*self.beta,3.5-self.beta,Erel) + return fE def _icmf(self,ms): '''Analytic expression for the normalized inverse cumulative mass From 4dc6d72859e1fc804e1ea5047fcb01b43c47f725 Mon Sep 17 00:00:00 2001 From: James Lane Date: Tue, 1 Sep 2020 08:52:14 -0700 Subject: [PATCH 38/91] Make isotropicHernquistdf a subclass of Eddingtondf --- galpy/df/isotropicHernquistdf.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index ad8457b40..5cca3a4e3 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -1,15 +1,12 @@ # Class that implements isotropic spherical Hernquist DF # computed using the Eddington formula -from .sphericaldf import sphericaldf from .Eddingtondf import Eddingtondf class isotropicHernquistdf(Eddingtondf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" def __init__(self,pot=None,ro=None,vo=None): - # Initialize using sphericaldf rather than Eddingtondf, because - # Eddingtondf will have code specific to computing the Eddington - # integral, which is not necessary for Hernquist - sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) + assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' + Eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) def __call_internal__(self,*args): """ From b074f9efd683e2645810067123c6cfa638476fad Mon Sep 17 00:00:00 2001 From: James Lane Date: Tue, 1 Sep 2020 08:52:31 -0700 Subject: [PATCH 39/91] Rename __call_internal__ to _call_internal --- galpy/df/Eddingtondf.py | 10 ++++-- galpy/df/constantbetaHernquistdf.py | 4 +-- galpy/df/constantbetadf.py | 6 ++++ galpy/df/isotropicHernquistdf.py | 54 ++++++++++++++++------------- galpy/df/sphericaldf.py | 2 +- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index 51da94214..caa710765 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -1,11 +1,17 @@ # Class that implements isotropic spherical DFs computed using the Eddington # formula from .sphericaldf import sphericaldf +import numpy +import scipy.interpolate class Eddingtondf(sphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula""" - def __init__(self,ro=None,vo=None): - sphericaldf.__init__(self,ro=ro,vo=vo) + def __init__(self,pot=None,ro=None,vo=None): + sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) + + def _call_internal(self,*args): + # Stub for calling + return None def fE(self,E): # Stub for computing f(E) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 96166e527..4817a9220 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -44,11 +44,11 @@ def __init__(self,pot=None,beta=0,use_BD02=True,ro=None,vo=None): self._use_BD02 = use_BD02 constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) - def __call_internal__(self,*args): + def _call_internal(self,*args): """ NAME: - __call_internal + _call_internal PURPOSE: diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index cb6801428..abba45c58 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -1,6 +1,8 @@ # Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant # beta anisotropy parameter from .sphericaldf import anisotropicsphericaldf +import numpy +import scipy.interpolate class constantbetadf(anisotropicsphericaldf): """Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant beta anisotropy parameter""" @@ -22,6 +24,10 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): anisotropicsphericaldf.__init__(self,pot=pot,dftype='constant', ro=ro,vo=vo) + def _call_internal(self,*args): + # Stub for calling + return None + def fE(self,E): # Stub for computing f_1(E) in BT08 nomenclature return None diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index 5cca3a4e3..b546e9bcd 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -1,6 +1,12 @@ # Class that implements isotropic spherical Hernquist DF # computed using the Eddington formula +import numpy +import pdb from .Eddingtondf import Eddingtondf +from ..potential import evaluatePotentials,HernquistPotential +from .df import _APY_LOADED +if _APY_LOADED: + from astropy import units class isotropicHernquistdf(Eddingtondf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" @@ -8,11 +14,11 @@ def __init__(self,pot=None,ro=None,vo=None): assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' Eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) - def __call_internal__(self,*args): + def _call_internal(self,*args): """ NAME: - __call_internal__ + _call_internal PURPOSE @@ -32,27 +38,7 @@ def __call_internal__(self,*args): """ E = args[0] - if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E= E.to(units.km**2/units.s**2).value/vo**2. - # Scale energies - phi0 = evaluatePotentials(self._pot,0,0) - Erel = -E - Etilde = Erel/phi0 - # Handle potential E out of bounds - Etilde_out = numpy.where(Etilde<0|Etilde>1)[0] - if len(Etilde_out)>0: - # Set to dummy and 0 later, prevents functions throwing errors? - Etilde[Etilde_out]=0.5 - _GMa = phi0*self._pot.a**2. - fH = numpy.power((2**0.5)*((2*numpy.pi)**3) *((_GMa)**1.5),-1)\ - *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ - *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ - +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ - /numpy.sqrt(Etilde*(1-Etilde)))) - # Fix out of bounds values - if len(Etilde_out) > 0: - fH[Etilde_out] = 0 - return fH + return self.fE(E) def fE(self,E): """ @@ -77,7 +63,27 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ - return self.__call_internal__(E) + if _APY_LOADED and isinstance(E,units.quantity.Quantity): + E= E.to(units.km**2/units.s**2).value/vo**2. + phi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) + Erel = -E + Etilde = Erel/phi0 + # Handle potential E out of bounds + Etilde_out = numpy.where(numpy.logical_or(Etilde<0,Etilde>1))[0] + if len(Etilde_out)>0: + # Set to dummy and 0 later, prevents functions throwing errors? + Etilde[Etilde_out]=0.5 + + _GMa = phi0*self._pot.a**2. + fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ + *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ + *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ + +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ + /numpy.sqrt(Etilde*(1-Etilde)))) + # Fix out of bounds values + if len(Etilde_out) > 0: + fE[Etilde_out] = 0 + return fE def _icmf(self,ms): '''Analytic expression for the normalized inverse cumulative mass diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 02299b2e5..d34ac7f77 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -146,7 +146,7 @@ def __call__(self,*args,**kwargs): r = numpy.sqrt(R**2.+z**2.) vrad = (R*vR+z*vz)/r L = numpy.sqrt(vtotSq-vrad**2.)*r - f = self.__call_internal__(E,L,Lz) # Some function for each sub-class + f = self._call_internal(E,L,Lz) # Some function for each sub-class return f ############################### SAMPLING THE DF################################ From 931e00ec7a4603ec6082091c982575639dda5a20 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Tue, 1 Sep 2020 20:02:01 -0400 Subject: [PATCH 40/91] Sample eta for the isotropic case directly using the analytic inverse CDF --- galpy/df/Eddingtondf.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index caa710765..adccbf33a 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -2,7 +2,6 @@ # formula from .sphericaldf import sphericaldf import numpy -import scipy.interpolate class Eddingtondf(sphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula""" @@ -19,12 +18,4 @@ def fE(self,E): def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" - deta = 0.00005*numpy.pi - etas = (numpy.arange(0, numpy.pi, deta)+deta/2) - eta_pdf_cml = numpy.cumsum(numpy.sin(etas)) - eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] - eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, - bounds_error=False, fill_value='extrapolate') - eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) - return eta_samples - + return numpy.arccos(1.-2.*numpy.random.uniform(size=n)) From c6eada9d6fde6a51bdb9c6fa4590ca21b5f180bb Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Tue, 1 Sep 2020 20:02:39 -0400 Subject: [PATCH 41/91] Bring v sampling out of the if/else --- galpy/df/sphericaldf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index d34ac7f77..a256bb9eb 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -193,7 +193,6 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): """ if R is None or z is None: # Full 6D samples r = self._sample_r(n=n) - v = self._sample_v(r,n=n) phi,theta = self._sample_position_angles(n=n) R = r*numpy.sin(theta) z = r*numpy.cos(theta) @@ -202,10 +201,9 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): assert len(R) == len(z) n = len(R) r = numpy.sqrt(R**2.+z**2.) - v = self._sample_v(r,n=n) if phi is None: # Otherwise assume phi input type matches R,z phi,_ = self._sample_position_angles(n=n) - + v = self._sample_v(r,n=n) eta,psi = self._sample_velocity_angles(n=n) vr = v*numpy.cos(eta) vtheta = v*numpy.sin(eta)*numpy.cos(psi) From f8df04c37a034bc810a78d42144060cc9fa4154c Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Tue, 1 Sep 2020 20:50:09 -0400 Subject: [PATCH 42/91] Switch to using scalefreekingdf in kingpotential, to allow kingpotential to be used inside of kingdf --- galpy/df/kingdf.py | 1 - galpy/potential/KingPotential.py | 28 +++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 02b5c9108..9d6e07680 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -74,7 +74,6 @@ def solve(self,npt=1001): r= numpy.zeros(npt) W= numpy.zeros(npt) dWdr= numpy.zeros(npt) - m= numpy.zeros(npt) # Initialize (r[0]=0 already) W[0]= self.W0 # Determine central density and r0 diff --git a/galpy/potential/KingPotential.py b/galpy/potential/KingPotential.py index 7f8342f5b..f71a42840 100644 --- a/galpy/potential/KingPotential.py +++ b/galpy/potential/KingPotential.py @@ -13,7 +13,7 @@ class KingPotential(interpSphericalPotential): where :math:`\mathcal{E}` is the binding energy. """ - def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,ro=None,vo=None): + def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,_sfkdf=None,ro=None,vo=None): """ NAME: @@ -33,10 +33,6 @@ def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,ro=None,vo=None): npt= (1001) number of points to use to solve for Psi(r) when solving the King DF - scfa= (1.) scale parameter used in the SCF representation of the potential - - scfN= (30) number of expansion coefficients to use in the SCF representation of the potential - ro=, vo= standard galpy unit scaling parameters OUTPUT: @@ -49,14 +45,20 @@ def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,ro=None,vo=None): """ # Set up King DF - from ..df.kingdf import kingdf - kdf= kingdf(W0,M=M,rt=rt,ro=ro,vo=vo) + if _sfkdf is None: + from ..df.kingdf import _scalefreekingdf + sfkdf= _scalefreekingdf(W0) + sfkdf.solve(npt) + else: + sfkdf= _sfkdf + mass_scale= M/sfkdf.mass + radius_scale= rt/sfkdf.rt interpSphericalPotential.__init__(\ self, - rforce=lambda r: kdf._mass_scale/kdf._radius_scale**2. - *numpy.interp(r/kdf._radius_scale, - kdf._scalefree_kdf._r, - kdf._scalefree_kdf._dWdr), - rgrid=kdf._scalefree_kdf._r*kdf._radius_scale, - Phi0=-kdf.W0*kdf._mass_scale/kdf._radius_scale, + rforce=lambda r: mass_scale/radius_scale**2. + *numpy.interp(r/radius_scale, + sfkdf._r, + sfkdf._dWdr), + rgrid=sfkdf._r*radius_scale, + Phi0=-W0*mass_scale/radius_scale, ro=ro,vo=vo) From ae9f5f8d2a07eed7589b5710bf5195644b083b2d Mon Sep 17 00:00:00 2001 From: James Lane Date: Tue, 1 Sep 2020 19:15:47 -0700 Subject: [PATCH 43/91] Change to RectBivariateSpline to correct sampling errors --- galpy/df/sphericaldf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index d34ac7f77..1c7b4a73f 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -276,8 +276,8 @@ def _sample_v(self,r,n=1): vesc_vals = vesc(self._pot,r,use_physical=False) pvr_icdf_samples = numpy.random.random(size=n) v_vesc_samples = self._v_vesc_pvr_interpolator(numpy.log10(r/self._scale), - pvr_icdf_samples) - return numpy.diag(v_vesc_samples)*vesc_vals + pvr_icdf_samples,grid=False) + return v_vesc_samples*vesc_vals def _sample_velocity_angles(self,n=1): """Generate samples of angles that set radial vs tangential velocities""" @@ -349,16 +349,16 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, cml_pvr_inv_interp = scipy.interpolate.interp1d(cml_pvr, v_vesc_values, kind='cubic', bounds_error=None, fill_value='extrapolate') - pvr_samples_reg = numpy.linspace(0,1,num=n_new_pvr,endpoint=False) + pvr_samples_reg = numpy.linspace(0,1,num=n_new_pvr) v_vesc_samples_reg = cml_pvr_inv_interp(pvr_samples_reg) icdf_pvr_grid_reg[:,i] = pvr_samples_reg icdf_v_vesc_grid_reg[:,i] = v_vesc_samples_reg ###i # Create the interpolator - v_vesc_icdf_interpolator = scipy.interpolate.interp2d( + v_vesc_icdf_interpolator = scipy.interpolate.RectBivariateSpline( numpy.log10(r_a_grid[0,:]), icdf_pvr_grid_reg[:,0], - icdf_v_vesc_grid_reg, bounds_error=False,fill_value=None) + icdf_v_vesc_grid_reg.T) return v_vesc_icdf_interpolator class anisotropicsphericaldf(sphericaldf): From 248819988a6adc48cb80beafefef9641c2db2069 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 10:58:31 -0400 Subject: [PATCH 44/91] Add test of the spherical symmetry of Hernquist DF samples --- tests/test_sphericaldf.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 75992a551..c4ebf6cab 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -1,6 +1,34 @@ # Tests of spherical distribution functions - -#### STUB, this just to see that everything imports, remove later ##### -from galpy.df import Eddingtondf +import numpy +from scipy import special +from galpy import potential from galpy.df import isotropicHernquistdf -from galpy.df import constantbetaHernquistdf + +# Test that the density distribution of the isotropic Hernquist is correct +def test_isotropic_hernquist_dens_spherically_symmetric(): + pot = potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + samp= dfh.sample(n=100000) + # Check spherical symmetry for different harmonics l,m + tol= 1e-2 + check_spherical_symmetry(samp,0,0,tol) + check_spherical_symmetry(samp,1,0,tol) + check_spherical_symmetry(samp,1,-1,tol) + check_spherical_symmetry(samp,1,1,tol) + check_spherical_symmetry(samp,2,0,tol) + check_spherical_symmetry(samp,2,-1,tol) + check_spherical_symmetry(samp,2,-2,tol) + check_spherical_symmetry(samp,2,1,tol) + check_spherical_symmetry(samp,2,2,tol) + # and some higher order ones + check_spherical_symmetry(samp,3,1,tol) + check_spherical_symmetry(samp,9,-6,tol) + return None + +def check_spherical_symmetry(samp,l,m,tol): + """Check for spherical symmetry by Monte Carlo integration of the + spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" + thetas, phis= numpy.arctan2(samp.R(),samp.z()), samp.phi() + assert numpy.fabs(numpy.sum(special.lpmv(m,l,numpy.cos(thetas))*numpy.cos(m*phis))/samp.size-(l==0)*(m==0)) < tol, 'Sample does not appear to be spherically symmetric, fails spherical harmonics test for (l,m) = ({},{})'.format(l,m) + return None + From 826b175f8ee0080937d24898667d59d6bec57dad Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 11:43:23 -0400 Subject: [PATCH 45/91] Add test of radial profile of isotropic Hernquist DF --- tests/test_sphericaldf.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index c4ebf6cab..af39e07ac 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -6,8 +6,9 @@ # Test that the density distribution of the isotropic Hernquist is correct def test_isotropic_hernquist_dens_spherically_symmetric(): - pot = potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) samp= dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol= 1e-2 @@ -25,10 +26,33 @@ def test_isotropic_hernquist_dens_spherically_symmetric(): check_spherical_symmetry(samp,9,-6,tol) return None +def test_isotropic_hernquist_dens_massprofile(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + tol= 5*1e-3 + check_spherical_massprofile(samp, + lambda r: pot.mass(r,use_physical=False)\ + /pot.mass(numpy.amax(samp.r(use_physical=False)), + use_physical=False), + tol,skip=1000) + return None + def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" - thetas, phis= numpy.arctan2(samp.R(),samp.z()), samp.phi() + thetas, phis= numpy.arctan2(samp.R(use_physical=False),samp.z(use_physical=False)), samp.phi(use_physical=False) assert numpy.fabs(numpy.sum(special.lpmv(m,l,numpy.cos(thetas))*numpy.cos(m*phis))/samp.size-(l==0)*(m==0)) < tol, 'Sample does not appear to be spherically symmetric, fails spherical harmonics test for (l,m) = ({},{})'.format(l,m) return None - + +def check_spherical_massprofile(samp,mass_profile,tol,skip=100): + """Check that the cumulative distribution of radii follows the + cumulative mass profile (normalized such that total mass = 1)""" + rs= samp.r(use_physical=False) + cumul_rs= numpy.sort(rs) + cumul_mass= numpy.linspace(0.,1.,len(rs)) + for ii in range(len(rs)//skip-1): + indx= (ii+1)*skip + assert numpy.fabs(cumul_mass[indx]-mass_profile(cumul_rs[indx])) < tol, 'Mass profile of samples does not agree with analytical one' + return None From a18cd5d103c0c97dfef652860cfeb05d0d6b3f82 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 11:59:27 -0400 Subject: [PATCH 46/91] Add test of sigmar of isotropic Hernquist profile --- tests/test_sphericaldf.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index af39e07ac..4c2804245 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -3,6 +3,7 @@ from scipy import special from galpy import potential from galpy.df import isotropicHernquistdf +from galpy.df import jeans # Test that the density distribution of the isotropic Hernquist is correct def test_isotropic_hernquist_dens_spherically_symmetric(): @@ -38,7 +39,17 @@ def test_isotropic_hernquist_dens_massprofile(): use_physical=False), tol,skip=1000) return None - + +def test_isotropic_hernquist_sigmar(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + tol= 0.05 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) + return None + def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" @@ -56,3 +67,26 @@ def check_spherical_massprofile(samp,mass_profile,tol,skip=100): indx= (ii+1)*skip assert numpy.fabs(cumul_mass[indx]-mass_profile(cumul_rs[indx])) < tol, 'Mass profile of samples does not agree with analytical one' return None + +def check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=None,rmax=None,bins=31): + """Check that sigma_r(r) obtained from a sampling agrees with that coming + from the Jeans equation + Does this by logarithmically binning in r between rmin and rmax""" + vrs= ((samp.vR(use_physical=False)*samp.R(use_physical=False) + +samp.vz(use_physical=False)*samp.z(use_physical=False))\ + /samp.r(use_physical=False)) + logrs= numpy.log(samp.r(use_physical=False)) + if rmin is None: numpy.exp(numpy.amin(logrs)) + if rmax is None: numpy.exp(numpy.amax(logrs)) + w,e= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=numpy.ones_like(logrs)) + mv2,_= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=vrs**2.) + samp_sigr= numpy.sqrt(mv2/w) + brs= numpy.exp((numpy.roll(e,-1)+e)[:-1]/2.) + for ii,br in enumerate(brs): + assert numpy.fabs(samp_sigr[ii]/jeans.sigmar(pot,br,beta=beta, + use_physical=False)-1.) < tol, \ + "sigma_r(r) from samples does not agree with that obtained from the Jeans equation" + return None From b58e4e314ce92f0dd279680f49c5094eff7ee7a9 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 13:03:53 -0400 Subject: [PATCH 47/91] Add test of beta for isotropic Hernquist --- tests/test_sphericaldf.py | 51 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 4c2804245..aabe16d85 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -50,6 +50,16 @@ def test_isotropic_hernquist_sigmar(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_isotropic_hernquist_beta(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(n=1000000) + tol= 6*1e-2 + check_beta(samp,pot,tol,beta=0., + rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) + return None + def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" @@ -73,9 +83,9 @@ def check_sigmar_against_jeans(samp,pot,tol,beta=0., """Check that sigma_r(r) obtained from a sampling agrees with that coming from the Jeans equation Does this by logarithmically binning in r between rmin and rmax""" - vrs= ((samp.vR(use_physical=False)*samp.R(use_physical=False) - +samp.vz(use_physical=False)*samp.z(use_physical=False))\ - /samp.r(use_physical=False)) + vrs= (samp.vR(use_physical=False)*samp.R(use_physical=False) + +samp.vz(use_physical=False)*samp.z(use_physical=False))\ + /samp.r(use_physical=False) logrs= numpy.log(samp.r(use_physical=False)) if rmin is None: numpy.exp(numpy.amin(logrs)) if rmax is None: numpy.exp(numpy.amax(logrs)) @@ -90,3 +100,38 @@ def check_sigmar_against_jeans(samp,pot,tol,beta=0., use_physical=False)-1.) < tol, \ "sigma_r(r) from samples does not agree with that obtained from the Jeans equation" return None + +def check_beta(samp,pot,tol,beta=0., + rmin=None,rmax=None,bins=31): + """Check that beta(r) obtained from a sampling agrees with the expected + value + Does this by logarithmically binning in r between rmin and rmax""" + vrs= (samp.vR(use_physical=False)*samp.R(use_physical=False) + +samp.vz(use_physical=False)*samp.z(use_physical=False))\ + /samp.r(use_physical=False) + vthetas=(samp.z(use_physical=False)*samp.vR(use_physical=False) + -samp.R(use_physical=False)*samp.vz(use_physical=False))\ + /samp.r(use_physical=False) + vphis= samp.vT(use_physical=False) + logrs= numpy.log(samp.r(use_physical=False)) + if rmin is None: numpy.exp(numpy.amin(logrs)) + if rmax is None: numpy.exp(numpy.amax(logrs)) + w,e= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=numpy.ones_like(logrs)) + mvr2,_= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=vrs**2.) + mvt2,_= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=vthetas**2.) + mvp2,_= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], + bins=bins,weights=vphis**2.) + samp_sigr= numpy.sqrt(mvr2/w) + samp_sigt= numpy.sqrt(mvt2/w) + samp_sigp= numpy.sqrt(mvp2/w) + samp_beta= 1.-(samp_sigt**2.+samp_sigp**2.)/2./samp_sigr**2. + brs= numpy.exp((numpy.roll(e,-1)+e)[:-1]/2.) + if not callable(beta): + beta_func= lambda r: beta + else: + beta_func= beta + assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value" + return None From 3fab6e70b25321d32175f212f3fc0dc87a4cd94b Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 13:21:28 -0400 Subject: [PATCH 48/91] Some re-arranging in anticipation of kingdf --- galpy/df/Eddingtondf.py | 38 +++++++++++++----- galpy/df/constantbetadf.py | 20 ++++++++- galpy/df/isotropicHernquistdf.py | 1 - galpy/df/sphericaldf.py | 69 +++++++++++++++++--------------- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index adccbf33a..7822b1326 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -1,12 +1,36 @@ # Class that implements isotropic spherical DFs computed using the Eddington # formula -from .sphericaldf import sphericaldf -import numpy +from ..potential import evaluatePotentials +from .sphericaldf import isotropicsphericaldf, _APY_LOADED +if _APY_LOADED: + from astropy import units -class Eddingtondf(sphericaldf): +class Eddingtondf(isotropicsphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula""" - def __init__(self,pot=None,ro=None,vo=None): - sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) + def __init__(self,pot=None,scale=None,ro=None,vo=None): + """ + scale - Characteristic scale radius to aid sampling calculations. + Not necessary, and will also be overridden by value from pot if + available. + """ + isotropicsphericaldf.__init__(self,ro=ro,vo=vo) + if pot is None: + raise IOError("pot= must be set") + # Some sort of check for spherical symmetry in the potential? + assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' + self._pot = pot + self._potInf = evaluatePotentials(pot,10**12,0) + try: + self._scale = pot._scale + except AttributeError: + if scale is not None: + if _APY_LOADED and isinstance(scale,units.Quantity): + scale= scale.to(units.kpc).value/self._ro + self._scale = scale + else: + self._scale = 1. + self._xi_cmf_interpolator = self._make_cmf_interpolator() + self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() def _call_internal(self,*args): # Stub for calling @@ -15,7 +39,3 @@ def _call_internal(self,*args): def fE(self,E): # Stub for computing f(E) return None - - def _sample_eta(self,n=1): - """Sample the angle eta which defines radial vs tangential velocities""" - return numpy.arccos(1.-2.*numpy.random.uniform(size=n)) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index abba45c58..08afb7610 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -20,9 +20,25 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): pot - Spherical potential which determines the DF """ + anisotropicsphericaldf.__init__(self,ro=ro,vo=vo) self.beta = beta - anisotropicsphericaldf.__init__(self,pot=pot,dftype='constant', - ro=ro,vo=vo) + if pot is None: + raise IOError("pot= must be set") + # Some sort of check for spherical symmetry in the potential? + assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' + self._pot = pot + self._potInf = evaluatePotentials(pot,10**12,0) + try: + self._scale = pot._scale + except AttributeError: + if scale is not None: + if _APY_LOADED and isinstance(scale,units.Quantity): + scale= scale.to(u.kpc).value/self._ro + self._scale = scale + else: + self._scale = 1. + self._xi_cmf_interpolator = self._make_cmf_interpolator() + self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() def _call_internal(self,*args): # Stub for calling diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index b546e9bcd..da71a34fd 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -1,7 +1,6 @@ # Class that implements isotropic spherical Hernquist DF # computed using the Eddington formula import numpy -import pdb from .Eddingtondf import Eddingtondf from ..potential import evaluatePotentials,HernquistPotential from .df import _APY_LOADED diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 0ba4b3dc8..a29805ab7 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -1,11 +1,10 @@ # Superclass for spherical distribution functions, contains # - sphericaldf: superclass of all spherical DFs +# - isotropicsphericaldf: superclass of all isotropic spherical DFs # - anisotropicsphericaldf: superclass of all anisotropic spherical DFs import numpy -import pdb import scipy.interpolate from .df import df, _APY_LOADED -from ..potential import flatten as flatten_potential from ..potential import evaluatePotentials, vesc from ..orbit import Orbit from ..util.bovy_conversion import physical_conversion @@ -14,7 +13,7 @@ class sphericaldf(df): """Superclass for spherical distribution functions""" - def __init__(self,pot=None,scale=None,ro=None,vo=None): + def __init__(self,ro=None,vo=None): """ NAME: @@ -26,11 +25,7 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): INPUT: - pot - Spherical potential which determines the DF - - scale - Characteristic scale radius to aid sampling calculations. - Not necessary, and will also be overridden by value from pot if - available. + ro= ,vo= galpy unit parameters OUTPUT: @@ -42,23 +37,6 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): """ df.__init__(self,ro=ro,vo=vo) - if pot is None: - raise IOError("pot= must be set") - # Some sort of check for spherical symmetry in the potential? - assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' - self._pot = pot - self._potInf = evaluatePotentials(pot,10**12,0) - try: - self._scale = pot._scale - except AttributeError: - if scale is not None: - if _APY_LOADED and isinstance(scale,units.Quantity): - scale= scale.to(u.kpc).value/self._ro - self._scale = scale - else: - self._scale = 1. - self._xi_cmf_interpolator = self._make_cmf_interpolator() - self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() ############################## EVALUATING THE DF############################### @physical_conversion('phasespacedensity',pop=True) @@ -359,9 +337,40 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, icdf_v_vesc_grid_reg.T) return v_vesc_icdf_interpolator +class isotropicsphericaldf(sphericaldf): + """Superclass for isotropic spherical distribution functions""" + def __init__(self,ro=None,vo=None): + """ + NAME: + + __init__ + + PURPOSE: + + Initialize an isotropic distribution function + + INPUT: + + ro=, vo= galpy unit parameters + + OUTPUT: + + None + + HISTORY: + + 2020-09-02 - Written - Bovy (UofT) + + """ + sphericaldf.__init__(self,ro=ro,vo=vo) + + def _sample_eta(self,n=1): + """Sample the angle eta which defines radial vs tangential velocities""" + return numpy.arccos(1.-2.*numpy.random.uniform(size=n)) + class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" - def __init__(self,pot=None,dftype=None,ro=None,vo=None): + def __init__(self,ro=None,vo=None): """ NAME: @@ -373,10 +382,7 @@ def __init__(self,pot=None,dftype=None,ro=None,vo=None): INPUT: - dftype= Type of anisotropic DF, either 'constant' for constant beta - over all r, or 'ossipkov-merrit' - - pot - Spherical potential which determines the DF + ro= ,vo= galpy unit parameters OUTPUT: @@ -387,5 +393,4 @@ def __init__(self,pot=None,dftype=None,ro=None,vo=None): 2020-07-22 - Written - """ - self._dftype = dftype - sphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) \ No newline at end of file + sphericaldf.__init__(self,ro=ro,vo=vo) From 2add520b9676aa2f131cc7086d4d72bdad0b0d7b Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 14:52:06 -0400 Subject: [PATCH 49/91] Integrate existing kingdf and general isotropicsphericaldf to allow sampling from KingDF --- galpy/df/__init__.py | 2 ++ galpy/df/kingdf.py | 35 ++++++++++++++++++++++++++++++----- galpy/df/sphericaldf.py | 12 ++++++++++-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/galpy/df/__init__.py b/galpy/df/__init__.py index 4c47d298c..610864340 100644 --- a/galpy/df/__init__.py +++ b/galpy/df/__init__.py @@ -8,6 +8,7 @@ from . import Eddingtondf from . import isotropicHernquistdf from . import constantbetaHernquistdf +from . import kingdf # # Functions # @@ -38,3 +39,4 @@ Eddingtondf= Eddingtondf.Eddingtondf isotropicHernquistdf= isotropicHernquistdf.isotropicHernquistdf constantbetaHernquistdf= constantbetaHernquistdf.constantbetaHernquistdf +kingdf= kingdf.kingdf diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 9d6e07680..8274aa376 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -1,12 +1,12 @@ # Class that represents a King DF import numpy from scipy import special, integrate, interpolate -from .sphericaldf import sphericaldf +from .sphericaldf import isotropicsphericaldf _FOURPI= 4.*numpy.pi _TWOOVERSQRTPI= 2./numpy.sqrt(numpy.pi) -class kingdf(sphericaldf): +class kingdf(isotropicsphericaldf): """Class that represents a King DF""" def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): """ @@ -39,7 +39,7 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): 2020-07-09 - Written - Bovy (UofT) """ - sphericaldf.__init__(self,ro=ro,vo=vo) + isotropicsphericaldf.__init__(self,ro=ro,vo=vo) # Need to add parsing of Quantity inputs... self.W0= W0 @@ -57,10 +57,33 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): self.r0= self._scalefree_kdf.r0*self._radius_scale self.c= self._scalefree_kdf.c # invariant self.rt= rt # for convenience - + self.M= M # for convenience + self.sigma= self._velocity_scale + self._sigma2= self.sigma**2. + self.rho1= self._density_scale + # Setup the potential + from ..potential import KingPotential + self._pot= KingPotential(W0=self.W0,M=self.M,rt=self.rt, + _sfkdf=self._scalefree_kdf) + self._potInf= self._pot(self.rt,0.) + # Setup inverse cumulative mass function for radius sampling + self._scale= self.r0 + self._icmf= interpolate.InterpolatedUnivariateSpline(\ + self._mass_scale*self._scalefree_kdf._cumul_mass/self.M, + self._radius_scale*self._scalefree_kdf._r, + k=3) + self._v_vesc_pvr_interpolator = self._make_pvr_interpolator(r_a_end=numpy.log10(self.rt/self._scale)) + def dens(self,r): return self._scalefree_kdf.dens(r/self._radius_scale)\ *self._density_scale + + def fE(self,E): + out= numpy.zeros(numpy.atleast_1d(E).shape) + varE= self._potInf-E + out[varE > 0.]= (numpy.exp(varE[varE > 0.]/self._sigma2)-1.)\ + *(2.*numpy.pi*self._sigma2)**-1.5*self.rho1 + return out class _scalefreekingdf(object): """Internal helper class to solve the scale-free King DF model, that is, the one that only depends on W = Psi/sigma^2""" @@ -122,7 +145,9 @@ def solve(self,npt=1001): mass_shells= numpy.array([\ integrate.quad(lambda r: _FOURPI*r**2*self.dens(r), rlo,rhi)[0] for rlo,rhi in zip(r[:-1],r[1:])]) - self._cumul_mass= numpy.cumsum(mass_shells) + self._cumul_mass= numpy.hstack((\ + integrate.quad(lambda r: _FOURPI*r**2*self.dens(r),0.,r[0])[0], + numpy.cumsum(mass_shells))) self.mass= self._cumul_mass[-1] return None diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index a29805ab7..e92bf7d49 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -322,8 +322,16 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, r_a_grid_reg = numpy.repeat(r_a_grid,n_new_pvr/r_a_grid.shape[0],axis=0) for i in range(pvr_grid_cml_norm.shape[1]): cml_pvr = pvr_grid_cml_norm[:,i] - cml_pvr_inv_interp = scipy.interpolate.interp1d(cml_pvr, - v_vesc_values, kind='cubic', bounds_error=None, + # Deal with the fact that the escape velocity might be beyond + # allowed velocities for the DF (e.g., King, where any start that + # escapes to r > rt is unbound, but v_esc is still defined by the + # escape to infinity) + try: + end_indx= numpy.amin(numpy.arange(len(cml_pvr))[cml_pvr == numpy.amax(cml_pvr)])+1 + except ValueError: + end_indx= len(cml_pvr) + cml_pvr_inv_interp = scipy.interpolate.interp1d(cml_pvr[:end_indx], + v_vesc_values[:end_indx], kind='cubic', bounds_error=None, fill_value='extrapolate') pvr_samples_reg = numpy.linspace(0,1,num=n_new_pvr) v_vesc_samples_reg = cml_pvr_inv_interp(pvr_samples_reg) From 31d7123bb1297814f185d713151e0e3dd5bc7c7e Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 14:52:17 -0400 Subject: [PATCH 50/91] Add some tests of kingdf --- tests/test_sphericaldf.py | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index aabe16d85..369889b50 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -2,10 +2,10 @@ import numpy from scipy import special from galpy import potential -from galpy.df import isotropicHernquistdf +from galpy.df import isotropicHernquistdf, kingdf from galpy.df import jeans -# Test that the density distribution of the isotropic Hernquist is correct +############################# ISOTROPIC HERNQUIST DF ########################## def test_isotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -60,6 +60,66 @@ def test_isotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +################################# KING DF ##################################### +def test_king_dens_spherically_symmetric(): + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + numpy.random.seed(10) + samp= dfk.sample(n=100000) + # Check spherical symmetry for different harmonics l,m + tol= 1e-2 + check_spherical_symmetry(samp,0,0,tol) + check_spherical_symmetry(samp,1,0,tol) + check_spherical_symmetry(samp,1,-1,tol) + check_spherical_symmetry(samp,1,1,tol) + check_spherical_symmetry(samp,2,0,tol) + check_spherical_symmetry(samp,2,-1,tol) + check_spherical_symmetry(samp,2,-2,tol) + check_spherical_symmetry(samp,2,1,tol) + check_spherical_symmetry(samp,2,2,tol) + # and some higher order ones + check_spherical_symmetry(samp,3,1,tol) + check_spherical_symmetry(samp,9,-6,tol) + return None + +def test_king_dens_massprofile(): + pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + numpy.random.seed(10) + samp= dfk.sample(n=100000) + tol= 1e-2 + check_spherical_massprofile(samp, + lambda r: pot.mass(r,use_physical=False)\ + /pot.mass(numpy.amax(samp.r(use_physical=False)), + use_physical=False), + tol,skip=4000) + return None + +def test_king_sigmar(): + pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + numpy.random.seed(10) + samp= dfk.sample(n=1000000) + # lower tolerance closer to rt because fewer stars there + tol= 0.07 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=dfk._scale/10.,rmax=dfk.rt*0.7,bins=31) + tol= 0.2 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=dfk.rt*0.8,rmax=dfk.rt,bins=5) + return None + +def test_king_beta(): + pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + numpy.random.seed(10) + samp= dfk.sample(n=1000000) + tol= 6*1e-2 + # lower tolerance closer to rt because fewer stars there + tol= 0.12 + check_beta(samp,pot,tol,beta=0.,rmin=dfk._scale/10.,rmax=dfk.rt, + bins=31) + return None + def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" From ac6e1edb2c4da54a6df3f31c20e76edea7069a70 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 15:54:49 -0400 Subject: [PATCH 51/91] Add some tests of the constant beta Hernquist DF and fix some issues along the way --- galpy/df/constantbetadf.py | 14 ++++++-- tests/test_sphericaldf.py | 66 +++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 08afb7610..39cc490d9 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -1,12 +1,15 @@ # Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant # beta anisotropy parameter -from .sphericaldf import anisotropicsphericaldf import numpy import scipy.interpolate +from ..potential import evaluatePotentials +from .sphericaldf import anisotropicsphericaldf, _APY_LOADED +if _APY_LOADED: + from astropy import units class constantbetadf(anisotropicsphericaldf): """Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant beta anisotropy parameter""" - def __init__(self,pot=None,beta=None,ro=None,vo=None): + def __init__(self,pot=None,beta=None,scale=None,ro=None,vo=None): """ NAME: @@ -19,6 +22,11 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): INPUT: pot - Spherical potential which determines the DF + + scale - Characteristic scale radius to aid sampling calculations. + Not necessary, and will also be overridden by value from pot if + available. + """ anisotropicsphericaldf.__init__(self,ro=ro,vo=vo) self.beta = beta @@ -33,7 +41,7 @@ def __init__(self,pot=None,beta=None,ro=None,vo=None): except AttributeError: if scale is not None: if _APY_LOADED and isinstance(scale,units.Quantity): - scale= scale.to(u.kpc).value/self._ro + scale= scale.to(units.kpc).value/self._ro self._scale = scale else: self._scale = 1. diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 369889b50..2d11490b3 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -2,7 +2,7 @@ import numpy from scipy import special from galpy import potential -from galpy.df import isotropicHernquistdf, kingdf +from galpy.df import isotropicHernquistdf, constantbetaHernquistdf, kingdf from galpy.df import jeans ############################# ISOTROPIC HERNQUIST DF ########################## @@ -60,6 +60,70 @@ def test_isotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +############################# ANISOTROPIC HERNQUIST DF ######################## +def test_anisotropic_hernquist_dens_spherically_symmetric(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + # Check spherical symmetry for different harmonics l,m + tol= 1e-2 + check_spherical_symmetry(samp,0,0,tol) + check_spherical_symmetry(samp,1,0,tol) + check_spherical_symmetry(samp,1,-1,tol) + check_spherical_symmetry(samp,1,1,tol) + check_spherical_symmetry(samp,2,0,tol) + check_spherical_symmetry(samp,2,-1,tol) + check_spherical_symmetry(samp,2,-2,tol) + check_spherical_symmetry(samp,2,1,tol) + check_spherical_symmetry(samp,2,2,tol) + # and some higher order ones + check_spherical_symmetry(samp,3,1,tol) + check_spherical_symmetry(samp,9,-6,tol) + return None + +def test_anisotropic_hernquist_dens_massprofile(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + tol= 5*1e-3 + check_spherical_massprofile(samp, + lambda r: pot.mass(r,use_physical=False)\ + /pot.mass(numpy.amax(samp.r(use_physical=False)), + use_physical=False), + tol,skip=1000) + return None + +def test_anisotropic_hernquist_sigmar(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + tol= 0.05 + check_sigmar_against_jeans(samp,pot,tol,beta=beta, + rmin=pot._scale/10.,rmax=pot._scale*10., + bins=31) + return None + +def test_anisotropic_hernquist_beta(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + numpy.random.seed(10) + samp= dfh.sample(n=1000000) + tol= 8*1e-2 + check_beta(samp,pot,tol,beta=beta, + rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) + return None + ################################# KING DF ##################################### def test_king_dens_spherically_symmetric(): dfk= kingdf(W0=3.,M=2.3,rt=1.76) From 217751bd89ecf15c130cd9d8f5c40fe89ac622e9 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 16:23:51 -0400 Subject: [PATCH 52/91] Some tweaks to the sphericaldf code, including introducing p(v|r) as what's used to set up the v interpolator --- galpy/df/Eddingtondf.py | 8 --- galpy/df/constantbetadf.py | 5 ++ galpy/df/isotropicHernquistdf.py | 2 +- galpy/df/sphericaldf.py | 96 +++++++++++++------------------- 4 files changed, 44 insertions(+), 67 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index 7822b1326..7ba9e8f49 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -31,11 +31,3 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): self._scale = 1. self._xi_cmf_interpolator = self._make_cmf_interpolator() self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() - - def _call_internal(self,*args): - # Stub for calling - return None - - def fE(self,E): - # Stub for computing f(E) - return None diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 39cc490d9..5e5d8a220 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -66,3 +66,8 @@ def _sample_eta(self,n=1): bounds_error=False, fill_value='extrapolate') eta_samples = eta_icml_interp(numpy.random.uniform(size=n)) return eta_samples + + def _p_v_at_r(self,v,r): + return self.fE(evaluatePotentials(self._pot,r,0,use_physical=False)\ + +0.5*v**2.)*v**(2.-2.*self.beta) + diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index da71a34fd..1702b402c 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -63,7 +63,7 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E= E.to(units.km**2/units.s**2).value/vo**2. + E= E.to(units.km**2/units.s**2).value/self._vo**2. phi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) Erel = -E Etilde = Erel/phi0 diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index e92bf7d49..ecad760f5 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -6,6 +6,7 @@ import scipy.interpolate from .df import df, _APY_LOADED from ..potential import evaluatePotentials, vesc +from ..potential.SCFPotential import _xiToR from ..orbit import Orbit from ..util.bovy_conversion import physical_conversion if _APY_LOADED: @@ -33,7 +34,7 @@ def __init__(self,ro=None,vo=None): HISTORY: - 2020-07-22 - Written - + 2020-07-22 - Written - Lane (UofT) """ df.__init__(self,ro=ro,vo=vo) @@ -62,18 +63,13 @@ def __call__(self,*args,**kwargs): c) Orbit instance: orbit.Orbit instance and if specific time then orbit.Orbit(t) - KWARGS: - - return_fE= if True then return the full distribution function plus - just the energy component (for e.g. an anisotropic DF) - OUTPUT: Value of DF HISTORY: - 2020-07-22 - Written - + 2020-07-22 - Written - Lane (UofT) """ # Stub for calling the DF as a function of either a) R,vR,vT,z,vz,phi, @@ -166,7 +162,7 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): HISTORY: - 2020-07-22 - Written - + 2020-07-22 - Written - Lane (UofT) """ if R is None or z is None: # Full 6D samples @@ -190,8 +186,9 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): vz = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) if return_orbit: - o = Orbit(vxvv=numpy.array([R,vR,vT,z,vz,phi]).T, - ro=self._ro,vo=self._vo) + o = Orbit(vxvv=numpy.array([R,vR,vT,z,vz,phi]).T) + if self._roSet and self._voSet: + o.turn_physical_on(ro=self._ro,vo=self._vo) return o else: return (R,vR,vT,z,vz,phi) @@ -201,16 +198,16 @@ def _sample_r(self,n=1): Note - the function interpolates the normalized CMF onto the variable xi defined as: - .. math:: \\xi = \\frac{r-1}{r+1} + .. math:: \\xi = \\frac{r/a-1}{r/a+1} so that xi is in the range [-1,1], which corresponds to an r range of [0,infinity)""" - rand_mass_frac = numpy.random.random(size=n) - if '_icmf' in dir(self): + rand_mass_frac = numpy.random.uniform(size=n) + if hasattr(self,'_icmf'): r_samples = self._icmf(rand_mass_frac) else: xi_samples = self._xi_cmf_interpolator(rand_mass_frac) - r_samples = self._xi_to_r(xi_samples,a=self._scale) + r_samples = _xiToR(xi_samples,a=self._scale) return r_samples def _make_cmf_interpolator(self): @@ -224,36 +221,26 @@ def _make_cmf_interpolator(self): so that xi is in the range [-1,1], which corresponds to an r range of [0,infinity)""" xis = numpy.arange(-1,1,1e-6) - rs = self._xi_to_r(xis,a=self._scale) + rs = _xiToR(xis,a=self._scale) ms = self._pot.mass(rs,use_physical=False) ms /= self._pot.mass(10**12,use_physical=False) + # Add total mass point xis = numpy.append(xis,1) ms = numpy.append(ms,1) - xis_cmf_interp = scipy.interpolate.interp1d(ms,xis, - kind='cubic',bounds_error=False,fill_value='extrapolate') - return xis_cmf_interp - - def _xi_to_r(self,xi,a=1): - """Calculate r from xi""" - return a*numpy.divide(1+xi,1-xi) - - def r_to_xi(self,r,a=1): - """Calculate xi from r""" - return numpy.divide(r/a-1,r/a+1) + return scipy.interpolate.InterpolatedUnivariateSpline(ms,xis,k=3) def _sample_position_angles(self,n=1): """Generate spherical angle samples""" phi_samples = numpy.random.uniform(size=n)*2*numpy.pi - theta_samples = numpy.arccos(2*numpy.random.uniform(size=n)-1) + theta_samples = numpy.arccos(1.-2*numpy.random.uniform(size=n)) return phi_samples,theta_samples def _sample_v(self,r,n=1): """Generate velocity samples""" - vesc_vals = vesc(self._pot,r,use_physical=False) - pvr_icdf_samples = numpy.random.random(size=n) - v_vesc_samples = self._v_vesc_pvr_interpolator(numpy.log10(r/self._scale), - pvr_icdf_samples,grid=False) - return v_vesc_samples*vesc_vals + return self._v_vesc_pvr_interpolator(\ + numpy.log10(r/self._scale),numpy.random.uniform(size=n), + grid=False)\ + *vesc(self._pot,r,use_physical=False) def _sample_velocity_angles(self,n=1): """Generate samples of angles that set radial vs tangential velocities""" @@ -262,9 +249,8 @@ def _sample_velocity_angles(self,n=1): return eta_samples,psi_samples def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, - r_a_interval=0.05, v_vesc_interval=0.01, set_interpolator=True, - output_grid=False): - ''' + r_a_interval=0.05, v_vesc_interval=0.01): + """ NAME: _make_pvr_interpolator @@ -296,30 +282,24 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, HISTORY: Written 2020-07-24 - James Lane (UofT) - ''' - # Make an array of r/a by v/vesc and then orbits to calculate fE - r_a_values = numpy.power(10,numpy.arange(r_a_start,r_a_end,r_a_interval)) + """ + # Make an array of r/a by v/vesc and then calculate p(v|r) + r_a_values = 10.**numpy.arange(r_a_start,r_a_end,r_a_interval) v_vesc_values = numpy.arange(0,1,v_vesc_interval) r_a_grid, v_vesc_grid = numpy.meshgrid(r_a_values,v_vesc_values) vesc_grid = vesc(self._pot,r_a_grid*self._scale,use_physical=False) - E_grid = evaluatePotentials(self._pot,r_a_grid*self._scale,0, - use_physical=False)+0.5*(numpy.multiply(v_vesc_grid,vesc_grid))**2. - - # Calculate cumulative p(v|r) - fE_grid = self.fE(E_grid).reshape(E_grid.shape) - _beta = 0 - if hasattr(self,'beta'): - _beta = self.beta - pvr_grid = numpy.multiply(fE_grid,(v_vesc_grid*vesc_grid)**(2-2*_beta)) - pvr_grid_cml = numpy.cumsum( pvr_grid, axis=0 ) + r_grid= r_a_grid*self._scale + vr_grid= v_vesc_grid*vesc_grid + # Calculate p(v|r) and normalize + pvr_grid= self._p_v_at_r(vr_grid,r_grid) + pvr_grid_cml = numpy.cumsum(pvr_grid,axis=0) pvr_grid_cml_norm = pvr_grid_cml\ /numpy.repeat(pvr_grid_cml[-1,:][:,numpy.newaxis],pvr_grid_cml.shape[0],axis=1).T - # Construct the inverse cumulative distribution + # Construct the inverse cumulative distribution on a regular grid n_new_pvr = 100 # Must be multiple of r_a_grid.shape[0] icdf_pvr_grid_reg = numpy.zeros((n_new_pvr,len(r_a_values))) icdf_v_vesc_grid_reg = numpy.zeros((n_new_pvr,len(r_a_values))) - r_a_grid_reg = numpy.repeat(r_a_grid,n_new_pvr/r_a_grid.shape[0],axis=0) for i in range(pvr_grid_cml_norm.shape[1]): cml_pvr = pvr_grid_cml_norm[:,i] # Deal with the fact that the escape velocity might be beyond @@ -330,20 +310,16 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, end_indx= numpy.amin(numpy.arange(len(cml_pvr))[cml_pvr == numpy.amax(cml_pvr)])+1 except ValueError: end_indx= len(cml_pvr) - cml_pvr_inv_interp = scipy.interpolate.interp1d(cml_pvr[:end_indx], - v_vesc_values[:end_indx], kind='cubic', bounds_error=None, - fill_value='extrapolate') - pvr_samples_reg = numpy.linspace(0,1,num=n_new_pvr) + cml_pvr_inv_interp = scipy.interpolate.InterpolatedUnivariateSpline(cml_pvr[:end_indx], + v_vesc_values[:end_indx],k=3) + pvr_samples_reg = numpy.linspace(0,1,n_new_pvr) v_vesc_samples_reg = cml_pvr_inv_interp(pvr_samples_reg) icdf_pvr_grid_reg[:,i] = pvr_samples_reg icdf_v_vesc_grid_reg[:,i] = v_vesc_samples_reg - ###i - # Create the interpolator - v_vesc_icdf_interpolator = scipy.interpolate.RectBivariateSpline( + return scipy.interpolate.RectBivariateSpline( numpy.log10(r_a_grid[0,:]), icdf_pvr_grid_reg[:,0], icdf_v_vesc_grid_reg.T) - return v_vesc_icdf_interpolator class isotropicsphericaldf(sphericaldf): """Superclass for isotropic spherical distribution functions""" @@ -376,6 +352,10 @@ def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" return numpy.arccos(1.-2.*numpy.random.uniform(size=n)) + def _p_v_at_r(self,v,r): + return self.fE(evaluatePotentials(self._pot,r,0,use_physical=False)\ + +0.5*v**2.)*v**2. + class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" def __init__(self,ro=None,vo=None): From abacda779091dbfe26edb8f71a4559076897b2bb Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 2 Sep 2020 16:28:56 -0400 Subject: [PATCH 53/91] Using use_physical=False in df sampline output no longer necessary --- tests/test_sphericaldf.py | 44 +++++++++++++++------------------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 2d11490b3..cb9973033 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -34,9 +34,9 @@ def test_isotropic_hernquist_dens_massprofile(): samp= dfh.sample(n=100000) tol= 5*1e-3 check_spherical_massprofile(samp, - lambda r: pot.mass(r,use_physical=False)\ - /pot.mass(numpy.amax(samp.r(use_physical=False)), - use_physical=False), + lambda r: pot.mass(r)\ + /pot.mass(numpy.amax(samp.r()), + ), tol,skip=1000) return None @@ -92,11 +92,9 @@ def test_anisotropic_hernquist_dens_massprofile(): numpy.random.seed(10) samp= dfh.sample(n=100000) tol= 5*1e-3 - check_spherical_massprofile(samp, - lambda r: pot.mass(r,use_physical=False)\ - /pot.mass(numpy.amax(samp.r(use_physical=False)), - use_physical=False), - tol,skip=1000) + check_spherical_massprofile(samp,lambda r: pot.mass(r)\ + /pot.mass(numpy.amax(samp.r())), + tol,skip=1000) return None def test_anisotropic_hernquist_sigmar(): @@ -151,10 +149,8 @@ def test_king_dens_massprofile(): numpy.random.seed(10) samp= dfk.sample(n=100000) tol= 1e-2 - check_spherical_massprofile(samp, - lambda r: pot.mass(r,use_physical=False)\ - /pot.mass(numpy.amax(samp.r(use_physical=False)), - use_physical=False), + check_spherical_massprofile(samp,lambda r: pot.mass(r)\ + /pot.mass(numpy.amax(samp.r())), tol,skip=4000) return None @@ -187,14 +183,14 @@ def test_king_beta(): def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" - thetas, phis= numpy.arctan2(samp.R(use_physical=False),samp.z(use_physical=False)), samp.phi(use_physical=False) + thetas, phis= numpy.arctan2(samp.R(),samp.z()), samp.phi() assert numpy.fabs(numpy.sum(special.lpmv(m,l,numpy.cos(thetas))*numpy.cos(m*phis))/samp.size-(l==0)*(m==0)) < tol, 'Sample does not appear to be spherically symmetric, fails spherical harmonics test for (l,m) = ({},{})'.format(l,m) return None def check_spherical_massprofile(samp,mass_profile,tol,skip=100): """Check that the cumulative distribution of radii follows the cumulative mass profile (normalized such that total mass = 1)""" - rs= samp.r(use_physical=False) + rs= samp.r() cumul_rs= numpy.sort(rs) cumul_mass= numpy.linspace(0.,1.,len(rs)) for ii in range(len(rs)//skip-1): @@ -207,10 +203,8 @@ def check_sigmar_against_jeans(samp,pot,tol,beta=0., """Check that sigma_r(r) obtained from a sampling agrees with that coming from the Jeans equation Does this by logarithmically binning in r between rmin and rmax""" - vrs= (samp.vR(use_physical=False)*samp.R(use_physical=False) - +samp.vz(use_physical=False)*samp.z(use_physical=False))\ - /samp.r(use_physical=False) - logrs= numpy.log(samp.r(use_physical=False)) + vrs= (samp.vR()*samp.R()+samp.vz()*samp.z())/samp.r() + logrs= numpy.log(samp.r()) if rmin is None: numpy.exp(numpy.amin(logrs)) if rmax is None: numpy.exp(numpy.amax(logrs)) w,e= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], @@ -221,7 +215,7 @@ def check_sigmar_against_jeans(samp,pot,tol,beta=0., brs= numpy.exp((numpy.roll(e,-1)+e)[:-1]/2.) for ii,br in enumerate(brs): assert numpy.fabs(samp_sigr[ii]/jeans.sigmar(pot,br,beta=beta, - use_physical=False)-1.) < tol, \ + )-1.) < tol, \ "sigma_r(r) from samples does not agree with that obtained from the Jeans equation" return None @@ -230,14 +224,10 @@ def check_beta(samp,pot,tol,beta=0., """Check that beta(r) obtained from a sampling agrees with the expected value Does this by logarithmically binning in r between rmin and rmax""" - vrs= (samp.vR(use_physical=False)*samp.R(use_physical=False) - +samp.vz(use_physical=False)*samp.z(use_physical=False))\ - /samp.r(use_physical=False) - vthetas=(samp.z(use_physical=False)*samp.vR(use_physical=False) - -samp.R(use_physical=False)*samp.vz(use_physical=False))\ - /samp.r(use_physical=False) - vphis= samp.vT(use_physical=False) - logrs= numpy.log(samp.r(use_physical=False)) + vrs= (samp.vR()*samp.R()+samp.vz()*samp.z())/samp.r() + vthetas=(samp.z()*samp.vR()-samp.R()*samp.vz())/samp.r() + vphis= samp.vT() + logrs= numpy.log(samp.r()) if rmin is None: numpy.exp(numpy.amin(logrs)) if rmax is None: numpy.exp(numpy.amax(logrs)) w,e= numpy.histogram(logrs,range=[numpy.log(rmin),numpy.log(rmax)], From 2a2ffc64dcb24664057bbafba02da1585eb1fab2 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 3 Sep 2020 18:22:37 -0400 Subject: [PATCH 54/91] Switch to using parsers for Quantity handling in sphericaldfs --- galpy/df/Eddingtondf.py | 9 +++----- galpy/df/constantbetaHernquistdf.py | 10 +++------ galpy/df/constantbetadf.py | 9 +++----- galpy/df/isotropicHernquistdf.py | 9 +++----- galpy/df/sphericaldf.py | 34 ++++++++++------------------- 5 files changed, 24 insertions(+), 47 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index 7ba9e8f49..d82a36f74 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -1,9 +1,8 @@ # Class that implements isotropic spherical DFs computed using the Eddington # formula +from ..util import conversion from ..potential import evaluatePotentials -from .sphericaldf import isotropicsphericaldf, _APY_LOADED -if _APY_LOADED: - from astropy import units +from .sphericaldf import isotropicsphericaldf class Eddingtondf(isotropicsphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula""" @@ -24,9 +23,7 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): self._scale = pot._scale except AttributeError: if scale is not None: - if _APY_LOADED and isinstance(scale,units.Quantity): - scale= scale.to(units.kpc).value/self._ro - self._scale = scale + self._scale= conversion.parse_length(scale,ro=self._ro) else: self._scale = 1. self._xi_cmf_interpolator = self._make_cmf_interpolator() diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 4817a9220..382380609 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -1,14 +1,11 @@ # Class that implements the anisotropic spherical Hernquist DF with constant # beta parameter import numpy -import pdb import scipy.special import scipy.integrate -from .constantbetadf import constantbetadf -from .df import _APY_LOADED +from ..util import conversion from ..potential import evaluatePotentials,HernquistPotential -if _APY_LOADED: - from astropy import units +from .constantbetadf import constantbetadf class constantbetaHernquistdf(constantbetadf): """Class that implements the anisotropic spherical Hernquist DF with constant beta parameter""" @@ -95,8 +92,7 @@ def fE(self,E): 2020-07-22 - Written """ - if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E= E.to(units.km**2/units.s**2).value/self._vo**2. + E= conversion.parse_energy(E,vo=self._vo) psi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) Erel = -E Etilde = Erel/psi0 diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 5e5d8a220..2aa9a57d7 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -2,10 +2,9 @@ # beta anisotropy parameter import numpy import scipy.interpolate +from ..util import conversion from ..potential import evaluatePotentials -from .sphericaldf import anisotropicsphericaldf, _APY_LOADED -if _APY_LOADED: - from astropy import units +from .sphericaldf import anisotropicsphericaldf class constantbetadf(anisotropicsphericaldf): """Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant beta anisotropy parameter""" @@ -40,9 +39,7 @@ def __init__(self,pot=None,beta=None,scale=None,ro=None,vo=None): self._scale = pot._scale except AttributeError: if scale is not None: - if _APY_LOADED and isinstance(scale,units.Quantity): - scale= scale.to(units.kpc).value/self._ro - self._scale = scale + self._scale= conversion.parse_length(scale,ro=self._ro) else: self._scale = 1. self._xi_cmf_interpolator = self._make_cmf_interpolator() diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index 1702b402c..ab1bbd91e 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -1,11 +1,9 @@ # Class that implements isotropic spherical Hernquist DF # computed using the Eddington formula import numpy -from .Eddingtondf import Eddingtondf +from ..util import conversion from ..potential import evaluatePotentials,HernquistPotential -from .df import _APY_LOADED -if _APY_LOADED: - from astropy import units +from .Eddingtondf import Eddingtondf class isotropicHernquistdf(Eddingtondf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" @@ -62,8 +60,7 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ - if _APY_LOADED and isinstance(E,units.quantity.Quantity): - E= E.to(units.km**2/units.s**2).value/self._vo**2. + E= conversion.parse_energy(E,vo=self._vo) phi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) Erel = -E Etilde = Erel/phi0 diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index ecad760f5..69ad646cd 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -4,13 +4,12 @@ # - anisotropicsphericaldf: superclass of all anisotropic spherical DFs import numpy import scipy.interpolate -from .df import df, _APY_LOADED +from .df import df from ..potential import evaluatePotentials, vesc from ..potential.SCFPotential import _xiToR from ..orbit import Orbit -from ..util.bovy_conversion import physical_conversion -if _APY_LOADED: - from astropy import units +from ..util import conversion +from ..util.conversion import physical_conversion class sphericaldf(df): """Superclass for spherical distribution functions""" @@ -90,30 +89,21 @@ def __call__(self,*args,**kwargs): E = args[0].E(pot=self._pot) L = numpy.sqrt(numpy.sum(numpy.square(args[0].L()))) Lz = args[0].Lz() - if _APY_LOADED and isinstance(E,units.Quantity): - E= E.to(units.km**2/units.s**2).value/self._vo**2. - if _APY_LOADED and isinstance(L,units.Quantity): - L= L.to(units.kpc*units.km/units.s).value/self._ro/self._vo - if _APY_LOADED and isinstance(Lz,units.Quantity): - Lz= Lz.to(units.kpc*units.km/units.s).value/self._ro/self._vo + E= conversion.parse_energy(E,vo=self._vo) + L= conversion.parse_angmom(L,ro=self._vo,vo=self._vo) + Lz= conversion.parse_angmom(Lz,ro=self._vo,vo=self._vo) else: # Assume R,vR,vT,z,vz,(phi) if len(args) == 5: R,vR,vT,z,vz = args phi = None else: R,vR,vT,z,vz,phi = args - if _APY_LOADED and isinstance(R,units.Quantity): - R= R.to(units.kpc).value/self._ro - if _APY_LOADED and isinstance(vR,units.Quantity): - vR= vR.to(units.km/units.s).value/self._vo - if _APY_LOADED and isinstance(vT,units.Quantity): - vT= vT.to(units.km/units.s).value/self._vo - if _APY_LOADED and isinstance(z,units.Quantity): - z= z.to(units.kpc).value/self._ro - if _APY_LOADED and isinstance(vz,units.Quantity): - vz= vz.to(units.km/units.s).value/self._vo - if _APY_LOADED and isinstance(phi,units.Quantity): - phi= phi.to(units.rad).value + R= conversion.parse_length(R,ro=self._ro) + vR= conversion.parse_velocity(vR,vo=self._vo) + vT= conversion.parse_velocity(vT,vo=self._vo) + z= conversion.parse_length(z,ro=self._ro) + vz= conversion.parse_velocity(vz,vo=self._vo) + phi= conversion.parse_angle(phi) vtotSq = vR**2.+vT**2.+vz**2. E = 0.5*vtotSq + evaluatePotentials(R,z) Lz = R*vT From 5f4946b7c2121364da13ff6454436fa64bc007bb Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 12:52:06 -0400 Subject: [PATCH 55/91] Some more re-arranging of the sphericaldf classes --- galpy/df/Eddingtondf.py | 19 +------- galpy/df/constantbetadf.py | 16 +------ galpy/df/isotropicHernquistdf.py | 34 +++++++------- galpy/df/kingdf.py | 26 ++++++----- galpy/df/sphericaldf.py | 76 ++++++++++++++++++++------------ 5 files changed, 80 insertions(+), 91 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index d82a36f74..026f6a619 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -1,6 +1,5 @@ # Class that implements isotropic spherical DFs computed using the Eddington # formula -from ..util import conversion from ..potential import evaluatePotentials from .sphericaldf import isotropicsphericaldf @@ -12,19 +11,5 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): Not necessary, and will also be overridden by value from pot if available. """ - isotropicsphericaldf.__init__(self,ro=ro,vo=vo) - if pot is None: - raise IOError("pot= must be set") - # Some sort of check for spherical symmetry in the potential? - assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' - self._pot = pot - self._potInf = evaluatePotentials(pot,10**12,0) - try: - self._scale = pot._scale - except AttributeError: - if scale is not None: - self._scale= conversion.parse_length(scale,ro=self._ro) - else: - self._scale = 1. - self._xi_cmf_interpolator = self._make_cmf_interpolator() - self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() + isotropicsphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) + self._potInf= evaluatePotentials(pot,10**12,0) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 2aa9a57d7..d8b92aae7 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -27,23 +27,9 @@ def __init__(self,pot=None,beta=None,scale=None,ro=None,vo=None): available. """ - anisotropicsphericaldf.__init__(self,ro=ro,vo=vo) + anisotropicsphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) self.beta = beta - if pot is None: - raise IOError("pot= must be set") - # Some sort of check for spherical symmetry in the potential? - assert not isinstance(pot,(list,tuple)), 'Lists of potentials not yet supported' - self._pot = pot self._potInf = evaluatePotentials(pot,10**12,0) - try: - self._scale = pot._scale - except AttributeError: - if scale is not None: - self._scale= conversion.parse_length(scale,ro=self._ro) - else: - self._scale = 1. - self._xi_cmf_interpolator = self._make_cmf_interpolator() - self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() def _call_internal(self,*args): # Stub for calling diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index ab1bbd91e..e1d8be0b6 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -10,6 +10,9 @@ class isotropicHernquistdf(Eddingtondf): def __init__(self,pot=None,ro=None,vo=None): assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' Eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) + self._phi0= -evaluatePotentials(self._pot,0,0,use_physical=False) + self._GMa = self._phi0*self._pot.a**2. + self._fEnorm= 1./numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 def _call_internal(self,*args): """ @@ -23,19 +26,18 @@ def _call_internal(self,*args): INPUT: - E - The energy + E,L,Lz - The energy, angular momemtum magnitude, and its z component (only E is used) OUTPUT: - fH - The distribution function + f(x,v) = f(E[x,v]) HISTORY: - 2020-07 - Written + 2020-07 - Written - Lane (UofT) """ - E = args[0] - return self.fE(E) + return self.fE(args[0]) def fE(self,E): """ @@ -45,8 +47,7 @@ def fE(self,E): PURPOSE - Calculate the energy portion of an isotropic Hernquist distribution - function + Calculate the energy portion of an isotropic Hernquist distribution function INPUT: @@ -60,22 +61,17 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ - E= conversion.parse_energy(E,vo=self._vo) - phi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) - Erel = -E - Etilde = Erel/phi0 - # Handle potential E out of bounds + Etilde= -conversion.parse_energy(E,vo=self._vo)/self._phi0 + # Handle E out of bounds Etilde_out = numpy.where(numpy.logical_or(Etilde<0,Etilde>1))[0] if len(Etilde_out)>0: # Set to dummy and 0 later, prevents functions throwing errors? Etilde[Etilde_out]=0.5 - - _GMa = phi0*self._pot.a**2. - fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ - *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ - *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ - +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ - /numpy.sqrt(Etilde*(1-Etilde)))) + sqrtEtilde= numpy.sqrt(Etilde) + fE= self._fEnorm*sqrtEtilde/(1-Etilde)**2.\ + *((1.-2.*Etilde)*(8.*Etilde**2.-8.*Etilde-3.)\ + +((3.*numpy.arcsin(sqrtEtilde))\ + /numpy.sqrt(Etilde*(1.-Etilde)))) # Fix out of bounds values if len(Etilde_out) > 0: fE[Etilde_out] = 0 diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 8274aa376..7a77d8d81 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -1,6 +1,8 @@ # Class that represents a King DF import numpy from scipy import special, integrate, interpolate +from ..util import conversion +from .df import df from .sphericaldf import isotropicsphericaldf _FOURPI= 4.*numpy.pi @@ -39,40 +41,42 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): 2020-07-09 - Written - Bovy (UofT) """ - isotropicsphericaldf.__init__(self,ro=ro,vo=vo) - # Need to add parsing of Quantity inputs... - + # Just run df init to set up unit-conversion parameters + df.__init__(self,ro=ro,vo=vo) self.W0= W0 + self.M= conversion.parse_mass(M,ro=self._ro,vo=self._vo) + self.rt= conversion.parse_length(rt,ro=self._ro) # Solve (mass,rtidal)-scale-free model, which is the basis for # the full solution self._scalefree_kdf= _scalefreekingdf(self.W0) self._scalefree_kdf.solve(npt) # Set up scaling factors - self._radius_scale= rt/self._scalefree_kdf.rt - self._mass_scale= M/self._scalefree_kdf.mass + self._radius_scale= self.rt/self._scalefree_kdf.rt + self._mass_scale= self.M/self._scalefree_kdf.mass self._velocity_scale= numpy.sqrt(self._mass_scale/self._radius_scale) self._density_scale= self._mass_scale/self._radius_scale**3. # Store central density, r0... self.rho0= self._scalefree_kdf.rho0*self._density_scale self.r0= self._scalefree_kdf.r0*self._radius_scale self.c= self._scalefree_kdf.c # invariant - self.rt= rt # for convenience - self.M= M # for convenience self.sigma= self._velocity_scale self._sigma2= self.sigma**2. self.rho1= self._density_scale # Setup the potential from ..potential import KingPotential - self._pot= KingPotential(W0=self.W0,M=self.M,rt=self.rt, - _sfkdf=self._scalefree_kdf) + pot= KingPotential(W0=self.W0,M=self.M,rt=self.rt, + _sfkdf=self._scalefree_kdf) + # Now initialize the isotropic DF + isotropicsphericaldf.__init__(self,pot=pot,scale=self.r0,ro=ro,vo=vo) self._potInf= self._pot(self.rt,0.) # Setup inverse cumulative mass function for radius sampling - self._scale= self.r0 self._icmf= interpolate.InterpolatedUnivariateSpline(\ self._mass_scale*self._scalefree_kdf._cumul_mass/self.M, self._radius_scale*self._scalefree_kdf._r, k=3) - self._v_vesc_pvr_interpolator = self._make_pvr_interpolator(r_a_end=numpy.log10(self.rt/self._scale)) + # Setup velocity DF interpolator for velocity sampling here + self._v_vesc_pvr_interpolator= self._make_pvr_interpolator(\ + r_a_end=numpy.log10(self.rt/self._scale)) def dens(self,r): return self._scalefree_kdf.dens(r/self._radius_scale)\ diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 69ad646cd..6582d96de 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -13,7 +13,7 @@ class sphericaldf(df): """Superclass for spherical distribution functions""" - def __init__(self,ro=None,vo=None): + def __init__(self,pot=None,scale=None,ro=None,vo=None): """ NAME: @@ -25,6 +25,10 @@ def __init__(self,ro=None,vo=None): INPUT: + pot= (None) Potential instance or list thereof + + scale= (None) length-scale parameter to be used internally + ro= ,vo= galpy unit parameters OUTPUT: @@ -37,6 +41,16 @@ def __init__(self,ro=None,vo=None): """ df.__init__(self,ro=ro,vo=vo) + if pot is None: + raise IOError("pot= must be set") + self._pot = pot + try: + self._scale = pot._scale + except AttributeError: + if scale is not None: + self._scale= conversion.parse_length(scale,ro=self._ro) + else: + self._scale = 1. ############################## EVALUATING THE DF############################### @physical_conversion('phasespacedensity',pop=True) @@ -71,10 +85,7 @@ def __call__(self,*args,**kwargs): 2020-07-22 - Written - Lane (UofT) """ - # Stub for calling the DF as a function of either a) R,vR,vT,z,vz,phi, - # b) Orbit, c) E,L (Lz?) --> maybe depends on the actual form? - - # Get E,L,Lz. Generic requirements for any possible spherical DF? + # Get E,L,Lz if len(args) == 1: if not isinstance(args[0],Orbit): # Assume tuple (E,L,Lz) if len(args[0]) == 1: @@ -86,32 +97,26 @@ def __call__(self,*args,**kwargs): elif len(args[0]) == 3: E,L,Lz = args[0] else: # Orbit - E = args[0].E(pot=self._pot) - L = numpy.sqrt(numpy.sum(numpy.square(args[0].L()))) - Lz = args[0].Lz() + E = args[0].E(pot=self._pot,use_physical=False) + L = numpy.sqrt(numpy.sum(args[0].L(use_physical=False)**2.)) + Lz = args[0].Lz(use_physical=False) E= conversion.parse_energy(E,vo=self._vo) L= conversion.parse_angmom(L,ro=self._vo,vo=self._vo) Lz= conversion.parse_angmom(Lz,ro=self._vo,vo=self._vo) else: # Assume R,vR,vT,z,vz,(phi) - if len(args) == 5: - R,vR,vT,z,vz = args - phi = None - else: - R,vR,vT,z,vz,phi = args + R,vR,vT,z,vz, *phi = args R= conversion.parse_length(R,ro=self._ro) vR= conversion.parse_velocity(vR,vo=self._vo) vT= conversion.parse_velocity(vT,vo=self._vo) z= conversion.parse_length(z,ro=self._ro) vz= conversion.parse_velocity(vz,vo=self._vo) - phi= conversion.parse_angle(phi) vtotSq = vR**2.+vT**2.+vz**2. E = 0.5*vtotSq + evaluatePotentials(R,z) Lz = R*vT r = numpy.sqrt(R**2.+z**2.) vrad = (R*vR+z*vz)/r L = numpy.sqrt(vtotSq-vrad**2.)*r - f = self._call_internal(E,L,Lz) # Some function for each sub-class - return f + return self._call_internal(E,L,Lz) # Some function for each sub-class ############################### SAMPLING THE DF################################ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): @@ -126,18 +131,17 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): INPUT: - R= Radius at which to generate samples (can be Quantity) + R= cylindrical radius at which to generate samples (can be Quantity) - z= Height at which to generate samples (can be Quantity) + z= height at which to generate samples (can be Quantity) - phi= Azimuth at which to generate samples (can be Quantity) + phi= azimuth at which to generate samples (can be Quantity) n= number of samples to generate OPTIONAL INPUT: - return_orbit= If True output is orbit.Orbit object, if False - output is (R,vR,vT,z,vz,phi) + return_orbit= (True) If True output is orbit.Orbit object, if False output is (R,vR,vT,z,vz,phi) OUTPUT: @@ -162,7 +166,9 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): z = r*numpy.cos(theta) else: # 3D velocity samples if isinstance(R,numpy.ndarray): - assert len(R) == len(z) + assert len(R) == len(z), \ + """When R= is set to an array, z= needs to be set to """\ + """an equal-length array""" n = len(R) r = numpy.sqrt(R**2.+z**2.) if phi is None: # Otherwise assume phi input type matches R,z @@ -174,7 +180,6 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): vT = v*numpy.sin(eta)*numpy.sin(psi) vR = vr*numpy.sin(theta) + vtheta*numpy.cos(theta) vz = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) - if return_orbit: o = Orbit(vxvv=numpy.array([R,vR,vT,z,vz,phi]).T) if self._roSet and self._voSet: @@ -196,6 +201,8 @@ def _sample_r(self,n=1): if hasattr(self,'_icmf'): r_samples = self._icmf(rand_mass_frac) else: + if not hasattr(self,'_xi_cmf_interpolator'): + self._xi_cmf_interpolator= self._make_cmf_interpolator() xi_samples = self._xi_cmf_interpolator(rand_mass_frac) r_samples = _xiToR(xi_samples,a=self._scale) return r_samples @@ -227,13 +234,16 @@ def _sample_position_angles(self,n=1): def _sample_v(self,r,n=1): """Generate velocity samples""" + if not hasattr(self,'_v_vesc_pvr_interpolator'): + self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() return self._v_vesc_pvr_interpolator(\ numpy.log10(r/self._scale),numpy.random.uniform(size=n), grid=False)\ *vesc(self._pot,r,use_physical=False) def _sample_velocity_angles(self,n=1): - """Generate samples of angles that set radial vs tangential velocities""" + """Generate samples of angles that set radial vs tangential + velocities""" eta_samples = self._sample_eta(n) psi_samples = numpy.random.uniform(size=n)*2*numpy.pi return eta_samples,psi_samples @@ -313,7 +323,7 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, class isotropicsphericaldf(sphericaldf): """Superclass for isotropic spherical distribution functions""" - def __init__(self,ro=None,vo=None): + def __init__(self,pot=None,scale=None,ro=None,vo=None): """ NAME: @@ -325,6 +335,10 @@ def __init__(self,ro=None,vo=None): INPUT: + pot= (None) Potential instance or list thereof + + scale= scale parameter to be used internally + ro=, vo= galpy unit parameters OUTPUT: @@ -336,7 +350,7 @@ def __init__(self,ro=None,vo=None): 2020-09-02 - Written - Bovy (UofT) """ - sphericaldf.__init__(self,ro=ro,vo=vo) + sphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" @@ -348,7 +362,7 @@ def _p_v_at_r(self,v,r): class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" - def __init__(self,ro=None,vo=None): + def __init__(self,pot=None,scale=None,ro=None,vo=None): """ NAME: @@ -360,6 +374,10 @@ def __init__(self,ro=None,vo=None): INPUT: + pot= (None) Potential instance or list thereof + + scale= (None) length-scale parameter to be used internally + ro= ,vo= galpy unit parameters OUTPUT: @@ -368,7 +386,7 @@ def __init__(self,ro=None,vo=None): HISTORY: - 2020-07-22 - Written - + 2020-07-22 - Written - Lane (UofT) """ - sphericaldf.__init__(self,ro=ro,vo=vo) + sphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) From ba2f8e9689bdeccef2f51a64d06a3c3ac3c9e53a Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 15:04:03 -0400 Subject: [PATCH 56/91] Switch to using _beta for internal beta, in anticipation of a beta function --- galpy/df/constantbetaHernquistdf.py | 58 ++++++++++++++--------------- galpy/df/constantbetadf.py | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index 382380609..d7e1c4302 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -68,7 +68,7 @@ def _call_internal(self,*args): E = args[0] L = args[1] fE = self.fE(E) - return L**(-2*self.beta)*fE + return L**(-2*self._beta)*fE def fE(self,E): """ @@ -104,22 +104,22 @@ def fE(self,E): # First check algebraic solutions _GMa = psi0*self._pot.a**2. - if self.beta == 0.: + if self._beta == 0.: fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ /numpy.sqrt(Etilde*(1-Etilde)))) - elif self.beta == 0.5: + elif self._beta == 0.5: fE = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) - elif self.beta == -0.5: + elif self._beta == -0.5: fE = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) elif self._use_BD02: fE = self._fE_BD02(Etilde) - elif self.beta < 1.0 and self.beta > 0.5: + elif self._beta < 1.0 and self._beta > 0.5: fE = self._fE_beta_gt05(Erel) - elif self.beta < 0.5 and self.beta > -0.5: + elif self._beta < 0.5 and self._beta > -0.5: fE = self._fE_beta_gtm05_lt05(Erel) if len(Etilde_out)>0: fE[Etilde_out] = 0 @@ -130,11 +130,11 @@ def _fE_beta_gt05(self,Erel): psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) _a = self._pot.a _GM = psi0*_a - Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self.beta)\ - /scipy.special.gamma(1.5-self.beta) - Cbeta = 2**(self.beta-0.5)/(2*numpy.pi*Ibeta) - alpha = self.beta-0.5 - coeff = (Cbeta*_a**(2*self.beta-2))*(numpy.sin(alpha*numpy.pi))\ + Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self._beta)\ + /scipy.special.gamma(1.5-self._beta) + Cbeta = 2**(self._beta-0.5)/(2*numpy.pi*Ibeta) + alpha = self._beta-0.5 + coeff = (Cbeta*_a**(2*self._beta-2))*(numpy.sin(alpha*numpy.pi))\ /(_GM*2*numpy.pi**2) if hasattr(Erel,'shape'): _Erel_shape = Erel.shape @@ -154,11 +154,11 @@ def _fE_beta_gt05_integral(self,psi,Erel,psi0): """Integral for calculating fE for a Hernquist when 0.5 < beta < 1.0""" psiTilde = psi/psi0 # Absolute value because the answer normally comes out imaginary? - denom = numpy.abs( (Erel-psi)**(1.5-self.beta) ) - numer = ((4-2*self.beta)*numpy.power(1-psiTilde,2*self.beta-1)\ - *numpy.power(psiTilde,3-2*self.beta)+(1-2*self.beta)\ - *numpy.power(1-psiTilde,2*self.beta-2)\ - *numpy.power(psiTilde,4-2*self.beta)) + denom = numpy.abs( (Erel-psi)**(1.5-self._beta) ) + numer = ((4-2*self._beta)*numpy.power(1-psiTilde,2*self._beta-1)\ + *numpy.power(psiTilde,3-2*self._beta)+(1-2*self._beta)\ + *numpy.power(1-psiTilde,2*self._beta-2)\ + *numpy.power(psiTilde,4-2*self._beta)) return numer/denom def _fE_beta_gtm05_lt05(self,Erel): @@ -166,11 +166,11 @@ def _fE_beta_gtm05_lt05(self,Erel): psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) _a = self._pot.a _GM = psi0*_a - alpha = 0.5-self.beta - Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self.beta)\ - /scipy.special.gamma(1.5-self.beta) - Cbeta = 2**(self.beta-0.5)/(2*numpy.pi*Ibeta*alpha) - coeff = (Cbeta*_a**(2*self.beta-1))*(numpy.sin(alpha*numpy.pi))\ + alpha = 0.5-self._beta + Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self._beta)\ + /scipy.special.gamma(1.5-self._beta) + Cbeta = 2**(self._beta-0.5)/(2*numpy.pi*Ibeta*alpha) + coeff = (Cbeta*_a**(2*self._beta-1))*(numpy.sin(alpha*numpy.pi))\ /((_GM**2)*2*numpy.pi**2) if hasattr(Erel,'shape'): _Erel_shape = Erel.shape @@ -190,19 +190,19 @@ def _fE_beta_gtm05_lt05_integral(self,psi,Erel,psi0): """Integral for calculating fE for a Hernquist when -0.5 < beta < 0.5""" psiTilde = psi/psi0 # Absolute value because the answer normally comes out imaginary? - denom = numpy.abs( (Erel-psi)**(0.5-self.beta) ) - numer = (4-2*self.beta-3*psiTilde)\ - *numpy.power(1-psiTilde,2*self.beta-2)\ - *numpy.power(psiTilde,3-2*self.beta) + denom = numpy.abs( (Erel-psi)**(0.5-self._beta) ) + numer = (4-2*self._beta-3*psiTilde)\ + *numpy.power(1-psiTilde,2*self._beta-2)\ + *numpy.power(psiTilde,3-2*self._beta) return numer/denom def _fE_BD02(self,Erel): """Calculate fE according to the hypergeometric solution of Baes & Dejonghe (2002)""" - coeff = (2.**self.beta/(2.*numpy.pi)**2.5)*scipy.special.gamma(5.-2.*self.beta)/\ - ( scipy.special.gamma(1.-self.beta)*scipy.special.gamma(3.5-self.beta) ) - fE = coeff*numpy.power(Erel,2.5-self.beta)*\ - scipy.special.hyp2f1(5.-2.*self.beta,1.-2.*self.beta,3.5-self.beta,Erel) + coeff = (2.**self._beta/(2.*numpy.pi)**2.5)*scipy.special.gamma(5.-2.*self._beta)/\ + ( scipy.special.gamma(1.-self._beta)*scipy.special.gamma(3.5-self._beta) ) + fE = coeff*numpy.power(Erel,2.5-self._beta)*\ + scipy.special.hyp2f1(5.-2.*self._beta,1.-2.*self._beta,3.5-self._beta,Erel) return fE def _icmf(self,ms): diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index d8b92aae7..908782aed 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -28,7 +28,7 @@ def __init__(self,pot=None,beta=None,scale=None,ro=None,vo=None): """ anisotropicsphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) - self.beta = beta + self._beta = beta self._potInf = evaluatePotentials(pot,10**12,0) def _call_internal(self,*args): @@ -43,7 +43,7 @@ def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" deta = 0.00005*numpy.pi etas = (numpy.arange(0, numpy.pi, deta)+deta/2) - eta_pdf_cml = numpy.cumsum(numpy.power(numpy.sin(etas),1.-2.*self.beta)) + eta_pdf_cml = numpy.cumsum(numpy.power(numpy.sin(etas),1.-2.*self._beta)) eta_pdf_cml_norm = eta_pdf_cml / eta_pdf_cml[-1] eta_icml_interp = scipy.interpolate.interp1d(eta_pdf_cml_norm, etas, bounds_error=False, fill_value='extrapolate') From 6d189a29e3879f070121d3751a62d3001cc4f89a Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 15:04:54 -0400 Subject: [PATCH 57/91] Add a function that gives the maximum velocity in the spherical DF at r, in case it's different from vesc --- galpy/df/kingdf.py | 10 +++++++--- galpy/df/sphericaldf.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 7a77d8d81..9237e9071 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -85,9 +85,13 @@ def dens(self,r): def fE(self,E): out= numpy.zeros(numpy.atleast_1d(E).shape) varE= self._potInf-E - out[varE > 0.]= (numpy.exp(varE[varE > 0.]/self._sigma2)-1.)\ - *(2.*numpy.pi*self._sigma2)**-1.5*self.rho1 - return out + if numpy.sum(varE > 0.) > 0: + out[varE > 0.]= (numpy.exp(varE[varE > 0.]/self._sigma2)-1.)\ + *(2.*numpy.pi*self._sigma2)**-1.5*self.rho1 + return out + + def _vmax_at_r(self,pot,r): + return numpy.sqrt(2.*(self._potInf-self._pot(r,0.,use_physical=False))) class _scalefreekingdf(object): """Internal helper class to solve the scale-free King DF model, that is, the one that only depends on W = Psi/sigma^2""" diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 6582d96de..9057886c6 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -238,8 +238,7 @@ def _sample_v(self,r,n=1): self._v_vesc_pvr_interpolator = self._make_pvr_interpolator() return self._v_vesc_pvr_interpolator(\ numpy.log10(r/self._scale),numpy.random.uniform(size=n), - grid=False)\ - *vesc(self._pot,r,use_physical=False) + grid=False)*self._vmax_at_r(self._pot,r) def _sample_velocity_angles(self,n=1): """Generate samples of angles that set radial vs tangential @@ -248,6 +247,12 @@ def _sample_velocity_angles(self,n=1): psi_samples = numpy.random.uniform(size=n)*2*numpy.pi return eta_samples,psi_samples + def _vmax_at_r(self,pot,r,**kwargs): + """Function that gives the max velocity in the DF at r; + typically equal to vesc, but not necessarily for finite systems + such as King""" + return vesc(pot,r,use_physical=False) + def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, r_a_interval=0.05, v_vesc_interval=0.01): """ @@ -287,7 +292,7 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, r_a_values = 10.**numpy.arange(r_a_start,r_a_end,r_a_interval) v_vesc_values = numpy.arange(0,1,v_vesc_interval) r_a_grid, v_vesc_grid = numpy.meshgrid(r_a_values,v_vesc_values) - vesc_grid = vesc(self._pot,r_a_grid*self._scale,use_physical=False) + vesc_grid = self._vmax_at_r(self._pot,r_a_grid*self._scale) r_grid= r_a_grid*self._scale vr_grid= v_vesc_grid*vesc_grid # Calculate p(v|r) and normalize From d7819292f445097340935e89105d35bb341b4974 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 15:06:07 -0400 Subject: [PATCH 58/91] Add function to compute moments of the DF and special functions sigmar, sigmat, beta --- galpy/df/constantbetadf.py | 43 ++++++++++++++++-- galpy/df/kingdf.py | 2 +- galpy/df/sphericaldf.py | 90 +++++++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 908782aed..e4b5dac61 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -2,8 +2,8 @@ # beta anisotropy parameter import numpy import scipy.interpolate -from ..util import conversion -from ..potential import evaluatePotentials +from scipy import integrate, special +from ..potential import evaluatePotentials, vesc from .sphericaldf import anisotropicsphericaldf class constantbetadf(anisotropicsphericaldf): @@ -52,5 +52,42 @@ def _sample_eta(self,n=1): def _p_v_at_r(self,v,r): return self.fE(evaluatePotentials(self._pot,r,0,use_physical=False)\ - +0.5*v**2.)*v**(2.-2.*self.beta) + +0.5*v**2.)*v**(2.-2.*self._beta) + def vmomentdensity(self,r,n,m): + """ + NAME: + + vmomentdensity + + PURPOSE: + + calculate the an arbitrary moment of the velocity distribution + at r times the density + + INPUT: + + r - spherical radius at which to calculate the moment + + n - vr^n, where vr = v x cos eta + + m - vt^m, where vt = v x sin eta + + OUTPUT: + + at r (no support for units) + + HISTORY: + + 2020-09-04 - Written - Bovy (UofT) + """ + if m%2 == 1 or n%2 == 1: + return 0. + return 2.*numpy.pi\ + *integrate.quad(lambda v: v**(2.-2.*self._beta+m+n) + *self.fE(evaluatePotentials(self._pot,r,0, + use_physical=False) + +0.5*v**2.), + 0.,self._vmax_at_r(self._pot,r))[0]\ + *special.gamma(m/2.-self._beta+1.)*special.gamma((n+1)/2.)/\ + 2./special.gamma(0.5*(m+n-2.*self._beta+3.)) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 9237e9071..388f6f8ee 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -68,7 +68,7 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): _sfkdf=self._scalefree_kdf) # Now initialize the isotropic DF isotropicsphericaldf.__init__(self,pot=pot,scale=self.r0,ro=ro,vo=vo) - self._potInf= self._pot(self.rt,0.) + self._potInf= self._pot(self.rt,0.,use_physical=False) # Setup inverse cumulative mass function for radius sampling self._icmf= interpolate.InterpolatedUnivariateSpline(\ self._mass_scale*self._scalefree_kdf._cumul_mass/self.M, diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 9057886c6..14e3528b1 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -4,6 +4,7 @@ # - anisotropicsphericaldf: superclass of all anisotropic spherical DFs import numpy import scipy.interpolate +from scipy import integrate, special from .df import df from ..potential import evaluatePotentials, vesc from ..potential.SCFPotential import _xiToR @@ -111,13 +112,62 @@ def __call__(self,*args,**kwargs): z= conversion.parse_length(z,ro=self._ro) vz= conversion.parse_velocity(vz,vo=self._vo) vtotSq = vR**2.+vT**2.+vz**2. - E = 0.5*vtotSq + evaluatePotentials(R,z) + E= 0.5*vtotSq+evaluatePotentials(self._pot,R,z,use_physical=False) Lz = R*vT r = numpy.sqrt(R**2.+z**2.) vrad = (R*vR+z*vz)/r L = numpy.sqrt(vtotSq-vrad**2.)*r return self._call_internal(E,L,Lz) # Some function for each sub-class + def vmomentdensity(self,r,n,m): + """ + NAME: + + vmomentdensity + + PURPOSE: + + calculate the an arbitrary moment of the velocity distribution + at r times the density + + INPUT: + + r - spherical radius at which to calculate the moment + + n - vr^n, where vr = v x cos eta + + m - vt^m, where vt = v x sin eta + + OUTPUT: + + at r (no support for units) + + HISTORY: + + 2020-09-04 - Written - Bovy (UofT) + """ + return 2.*numpy.pi\ + *integrate.dblquad(lambda eta,v: v**(2.+m+n) + *numpy.sin(eta)**(1+m)*numpy.cos(eta)**n + *self(r,v*numpy.cos(eta),v*numpy.sin(eta),0.,0., + use_physical=False), + 0.,self._vmax_at_r(self._pot,r), + lambda x: 0.,lambda x: numpy.pi)[0] + + @physical_conversion('velocity',pop=True) + def sigmar(self,r): + return numpy.sqrt(self.vmomentdensity(r,2,0) + /self.vmomentdensity(r,0,0)) + + @physical_conversion('velocity',pop=True) + def sigmat(self,r): + return numpy.sqrt(self.vmomentdensity(r,0,2) + /self.vmomentdensity(r,0,0)) + + def beta(self,r): + return 1.-self.sigmat(r,use_physical=False)**2./2.\ + /self.sigmar(r,use_physical=False)**2. + ############################### SAMPLING THE DF################################ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): """ @@ -357,6 +407,44 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): """ sphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) + def vmomentdensity(self,r,n,m): + """ + NAME: + + vmomentdensity + + PURPOSE: + + calculate the an arbitrary moment of the velocity distribution + at r times the density + + INPUT: + + r - spherical radius at which to calculate the moment + + n - vr^n, where vr = v x cos eta + + m - vt^m, where vt = v x sin eta + + OUTPUT: + + at r (no support for units) + + HISTORY: + + 2020-09-04 - Written - Bovy (UofT) + """ + if m%2 == 1 or n%2 == 1: + return 0. + return 2.*numpy.pi\ + *integrate.quad(lambda v: v**(2.+m+n)* + self.fE(evaluatePotentials(self._pot,r,0, + use_physical=False) + +0.5*v**2.), + 0.,self._vmax_at_r(self._pot,r))[0]\ + *special.gamma(m//2+1)*special.gamma(n//2+0.5)\ + /2./special.gamma(m//2+n//2+1.5) + def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" return numpy.arccos(1.-2.*numpy.random.uniform(size=n)) From f81288c25fa403e456d069b946048d2892316bcc Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 15:06:19 -0400 Subject: [PATCH 59/91] Add tests of new sigmar and beta functions --- tests/test_sphericaldf.py | 88 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index cb9973033..070f77eb2 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -59,7 +59,27 @@ def test_isotropic_hernquist_beta(): check_beta(samp,pot,tol,beta=0., rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None - + +def test_isotropic_hernquist_sigmar_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + tol= 1e-5 + check_sigmar_against_jeans_directint(dfh,pot,tol,beta=0., + rmin=pot._scale/10., + rmax=pot._scale*10., + bins=31) + return None + +def test_isotropic_hernquist_beta_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + tol= 1e-8 + check_beta_directint(dfh,tol,beta=0., + rmin=pot._scale/10., + rmax=pot._scale*10., + bins=31) + return None + ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) @@ -122,6 +142,30 @@ def test_anisotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_anisotropic_hernquist_sigmar_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + tol= 1e-5 + check_sigmar_against_jeans_directint(dfh,pot,tol,beta=beta, + rmin=pot._scale/10., + rmax=pot._scale*10., + bins=31) + return None + +def test_anisotropic_hernquist_beta_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + tol= 1e-8 + check_beta_directint(dfh,tol,beta=beta, + rmin=pot._scale/10., + rmax=pot._scale*10., + bins=31) + return None + ################################# KING DF ##################################### def test_king_dens_spherically_symmetric(): dfk= kingdf(W0=3.,M=2.3,rt=1.76) @@ -180,6 +224,23 @@ def test_king_beta(): bins=31) return None +def test_king_sigmar_directint(): + pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + tol= 0.05 # Jeans isn't that accurate for this rather difficult case + check_sigmar_against_jeans_directint(dfk,pot,tol,beta=0., + rmin=dfk._scale/10., + rmax=dfk.rt*0.7,bins=31) + return None + +def test_king_beta_directint(): + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + tol= 1e-8 + check_beta_directint(dfk,tol,beta=0., + rmin=dfk._scale/10.,rmax=dfk.rt*0.7,bins=31) + return None + +############################### HELPER FUNCTIONS ############################## def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" @@ -249,3 +310,28 @@ def check_beta(samp,pot,tol,beta=0., beta_func= beta assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value" return None + +def check_sigmar_against_jeans_directint(dfi,pot,tol,beta=0., + rmin=None,rmax=None,bins=31): + """Check that sigma_r(r) obtained from integrating over the DF agrees + with that coming from the Jeans equation""" + rs= numpy.linspace(rmin,rmax,bins) + intsr= numpy.array([dfi.sigmar(r,use_physical=False) for r in rs]) + jeanssr= numpy.array([jeans.sigmar(pot,r,beta=beta,use_physical=False) for r in rs]) + assert numpy.all(numpy.fabs(intsr/jeanssr-1) < tol), \ + "sigma_r(r) from direct integration does not agree with that obtained from the Jeans equation" + return None + +def check_beta_directint(dfi,tol,beta=0.,rmin=None,rmax=None,bins=31): + """Check that beta(r) obtained from integrating over the DF agrees + with the expected behavior""" + rs= numpy.linspace(rmin,rmax,bins) + intbeta= numpy.array([dfi.beta(r) for r in rs]) + if not callable(beta): + beta_func= lambda r: beta + else: + beta_func= beta + assert numpy.all(numpy.fabs(intbeta-beta_func(rs)) < tol), \ + "beta(r) from direct integration does not agree with the expected value" + return None + From c4184cbe538131ed25784fb07fc504dafe6a62d9 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 15:53:22 -0400 Subject: [PATCH 60/91] Turn units of spherical DF on if those of the underlying potential are on; requires a bit of gymnastics for King... --- galpy/df/kingdf.py | 6 +++--- galpy/df/sphericaldf.py | 7 +++++++ galpy/potential/KingPotential.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index 388f6f8ee..a7d2f3c64 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -62,10 +62,10 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): self.sigma= self._velocity_scale self._sigma2= self.sigma**2. self.rho1= self._density_scale - # Setup the potential + # Setup the potential, use original params in case they had units + # because then the initialization will turn on units for this object from ..potential import KingPotential - pot= KingPotential(W0=self.W0,M=self.M,rt=self.rt, - _sfkdf=self._scalefree_kdf) + pot= KingPotential(W0=self.W0,M=M,rt=rt,_sfkdf=self._scalefree_kdf) # Now initialize the isotropic DF isotropicsphericaldf.__init__(self,pot=pot,scale=self.r0,ro=ro,vo=vo) self._potInf= self._pot(self.rt,0.,use_physical=False) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 14e3528b1..5671bf409 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -42,6 +42,13 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): """ df.__init__(self,ro=ro,vo=vo) + if not conversion.physical_compatible(self,pot): + raise RuntimeError("Unit-conversion parameters of input potential incompatible with those of the DF instance") + phys= conversion.get_physical(pot,include_set=True) + # if pot has physical units, transfer them (if already on, we know + # they are compaible) + if phys['roSet'] and phys['voSet']: + self.turn_physical_on(ro=phys['ro'],vo=phys['vo']) if pot is None: raise IOError("pot= must be set") self._pot = pot diff --git a/galpy/potential/KingPotential.py b/galpy/potential/KingPotential.py index f71a42840..b7ff6b121 100644 --- a/galpy/potential/KingPotential.py +++ b/galpy/potential/KingPotential.py @@ -2,6 +2,8 @@ # KingPotential.py: Potential of a King profile ############################################################################### import numpy +from ..util import conversion +from .Force import Force from .interpSphericalPotential import interpSphericalPotential class KingPotential(interpSphericalPotential): """KingPotential.py: Potential of a King profile, defined from the distribution function @@ -44,6 +46,13 @@ def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,_sfkdf=None,ro=None,vo=None): 2020-07-11 - Written - Bovy (UofT) """ + # Initialize with Force just to parse (ro,vo) + Force.__init__(self,ro=ro,vo=vo) + newM= conversion.parse_mass(M,ro=self._ro,vo=self._vo) + if newM != M: + self.turn_physical_on(ro=self._ro,vo=self._vo) + M= newM + rt= conversion.parse_length(rt,ro=self._ro) # Set up King DF if _sfkdf is None: from ..df.kingdf import _scalefreekingdf @@ -53,6 +62,9 @@ def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,_sfkdf=None,ro=None,vo=None): sfkdf= _sfkdf mass_scale= M/sfkdf.mass radius_scale= rt/sfkdf.rt + # Remember whether to turn units on + ro= self._ro if self._roSet else ro + vo= self._vo if self._voSet else vo interpSphericalPotential.__init__(\ self, rforce=lambda r: mass_scale/radius_scale**2. From 724c10919fd7704c35023ab436b7b1a671fa6c1e Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 4 Sep 2020 20:54:58 -0400 Subject: [PATCH 61/91] Fix Python 2.7 issue with default tuple value --- galpy/df/sphericaldf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 5671bf409..67954116f 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -112,7 +112,7 @@ def __call__(self,*args,**kwargs): L= conversion.parse_angmom(L,ro=self._vo,vo=self._vo) Lz= conversion.parse_angmom(Lz,ro=self._vo,vo=self._vo) else: # Assume R,vR,vT,z,vz,(phi) - R,vR,vT,z,vz, *phi = args + R,vR,vT,z,vz, phi = (args+(None,))[:6] R= conversion.parse_length(R,ro=self._ro) vR= conversion.parse_velocity(vR,vo=self._vo) vT= conversion.parse_velocity(vT,vo=self._vo) From 9127557fff3e75222a4196f091f89f1d69bbfad0 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Sat, 5 Sep 2020 08:49:15 -0400 Subject: [PATCH 62/91] Simplify parsing of E and optional L and Lz --- galpy/df/sphericaldf.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 67954116f..27b7092c0 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -96,14 +96,7 @@ def __call__(self,*args,**kwargs): # Get E,L,Lz if len(args) == 1: if not isinstance(args[0],Orbit): # Assume tuple (E,L,Lz) - if len(args[0]) == 1: - E = args[0][0] - L,Lz = None,None - elif len(args[0]) == 2: - E,L = args[0] - Lz = None - elif len(args[0]) == 3: - E,L,Lz = args[0] + E,L,Lz= (args[0]+(None,None))[:3] else: # Orbit E = args[0].E(pot=self._pot,use_physical=False) L = numpy.sqrt(numpy.sum(args[0].L(use_physical=False)**2.)) From 2c54027c53f1e3df50a55178f0198df5ebe9159b Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Sat, 5 Sep 2020 09:07:42 -0400 Subject: [PATCH 63/91] Add a few simple tests of the mean radial velocity --- tests/test_sphericaldf.py | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 070f77eb2..dcbe35fe3 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -60,6 +60,14 @@ def test_isotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_isotropic_hernquist_meanvr_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + tol= 1e-8 + check_meanvr_directint(dfh,pot,tol,beta=0.,rmin=pot._scale/10., + rmax=pot._scale*10.,bins=31) + return None + def test_isotropic_hernquist_sigmar_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -70,6 +78,16 @@ def test_isotropic_hernquist_sigmar_directint(): bins=31) return None +def test_isotropic_hernquist_sigmar_directint_forcevmoment(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + tol= 1e-5 + check_sigmar_against_jeans_directint_forcevmoment(dfh,pot,tol,beta=0., + rmin=pot._scale/10., + rmax=pot._scale*10., + bins=31) + return None + def test_isotropic_hernquist_beta_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -142,6 +160,16 @@ def test_anisotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_anisotropic_hernquist_meanvr_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.4,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + tol= 1e-8 + check_meanvr_directint(dfh,pot,tol,beta=beta,rmin=pot._scale/10., + rmax=pot._scale*10.,bins=31) + return None + def test_anisotropic_hernquist_sigmar_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) betas= [-0.4,0.5] @@ -311,6 +339,17 @@ def check_beta(samp,pot,tol,beta=0., assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value" return None +def check_meanvr_directint(dfi,pot,tol,beta=0., + rmin=None,rmax=None,bins=31): + """Check that the mean v_r(r) obtained from integrating over the DF agrees + with the expected zero""" + rs= numpy.linspace(rmin,rmax,bins) + intmvr= numpy.array([dfi.vmomentdensity(r,1,0)/dfi.vmomentdensity(r,0,0) + for r in rs]) + assert numpy.all(numpy.fabs(intmvr) < tol), \ + "mean v_r(r) from direct integration is not zero" + return None + def check_sigmar_against_jeans_directint(dfi,pot,tol,beta=0., rmin=None,rmax=None,bins=31): """Check that sigma_r(r) obtained from integrating over the DF agrees @@ -322,6 +361,22 @@ def check_sigmar_against_jeans_directint(dfi,pot,tol,beta=0., "sigma_r(r) from direct integration does not agree with that obtained from the Jeans equation" return None +def check_sigmar_against_jeans_directint_forcevmoment(dfi,pot,tol,beta=0., + rmin=None,rmax=None, + bins=31): + """Check that sigma_r(r) obtained from integrating over the DF agrees + with that coming from the Jeans equation, using the general sphericaldf + class' vmomentdensity""" + from galpy.df.sphericaldf import sphericaldf + rs= numpy.linspace(rmin,rmax,bins) + intsr= numpy.array([numpy.sqrt(sphericaldf.vmomentdensity(dfi,r,2,0)/ + sphericaldf.vmomentdensity(dfi,r,0,0)) + for r in rs]) + jeanssr= numpy.array([jeans.sigmar(pot,r,beta=beta,use_physical=False) for r in rs]) + assert numpy.all(numpy.fabs(intsr/jeanssr-1) < tol), \ + "sigma_r(r) from direct integration does not agree with that obtained from the Jeans equation" + return None + def check_beta_directint(dfi,tol,beta=0.,rmin=None,rmax=None,bins=31): """Check that beta(r) obtained from integrating over the DF agrees with the expected behavior""" From 10f3090de9aa78ef39955634df1171305d8ba5dc Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 17:26:39 -0400 Subject: [PATCH 64/91] Simplify constantbetaHernquist, add tests of special cases --- galpy/df/constantbetaHernquistdf.py | 149 ++++++---------------------- tests/test_sphericaldf.py | 20 ++-- 2 files changed, 42 insertions(+), 127 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index d7e1c4302..d4e1dc14a 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -9,7 +9,7 @@ class constantbetaHernquistdf(constantbetadf): """Class that implements the anisotropic spherical Hernquist DF with constant beta parameter""" - def __init__(self,pot=None,beta=0,use_BD02=True,ro=None,vo=None): + def __init__(self,pot=None,beta=0,ro=None,vo=None): """ NAME: @@ -17,16 +17,13 @@ def __init__(self,pot=None,beta=0,use_BD02=True,ro=None,vo=None): PURPOSE: - Initialize a DF with constant anisotropy + Initialize a Hernquist DF with constant anisotropy INPUT: pot - Hernquist potential which determines the DF - beta - anisotropy parameter, must be in range [-0.5, 1.0) - - use_BD02 - Use Baes & Dejonghe (2002) solution for f(E) when - non-trivial algebraic solution does not exist + beta - anisotropy parameter OUTPUT: @@ -34,12 +31,17 @@ def __init__(self,pot=None,beta=0,use_BD02=True,ro=None,vo=None): HISTORY: - 2020-07-22 - Written + 2020-07-22 - Written - Lane (UofT) """ assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' - assert -0.5 <= beta and beta < 1.0,'Beta must be in range [-0.5,1.0)' - self._use_BD02 = use_BD02 constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) + self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) + self._GMa = self._psi0*self._pot.a**2. + self._fEnorm= (2.**self._beta/(2.*numpy.pi)**2.5)\ + *scipy.special.gamma(5.-2.*self._beta)/\ + ( scipy.special.gamma(1.-self._beta)*scipy.special.gamma(3.5-self._beta) ) + + def _call_internal(self,*args): """ @@ -63,12 +65,10 @@ def _call_internal(self,*args): HISTORY: - 2020-07-22 - Written + 2020-07-22 - Written - Lane (UofT) """ - E = args[0] - L = args[1] - fE = self.fE(E) - return L**(-2*self._beta)*fE + E, L= args + return L**(-2*self._beta)*self.fE(E) def fE(self,E): """ @@ -92,119 +92,34 @@ def fE(self,E): 2020-07-22 - Written """ - E= conversion.parse_energy(E,vo=self._vo) - psi0 = -evaluatePotentials(self._pot,0,0,use_physical=False) - Erel = -E - Etilde = Erel/psi0 + Etilde= -conversion.parse_energy(E,vo=self._vo)/self._psi0 # Handle potential E outside of bounds Etilde_out = numpy.where(numpy.logical_or(Etilde<0,Etilde>1))[0] if len(Etilde_out)>0: # Dummy variable now and 0 later, prevents numerical issues? Etilde[Etilde_out]=0.5 - # First check algebraic solutions - _GMa = psi0*self._pot.a**2. - if self._beta == 0.: - fE = numpy.power((2**0.5)*((2*numpy.pi)**3)*((_GMa)**1.5),-1)\ - *(numpy.sqrt(Etilde)/numpy.power(1-Etilde,2))\ - *((1-2*Etilde)*(8*numpy.power(Etilde,2)-8*Etilde-3)\ - +((3*numpy.arcsin(numpy.sqrt(Etilde)))\ - /numpy.sqrt(Etilde*(1-Etilde)))) + if self._beta == 0.: # isotropic case + sqrtEtilde= numpy.sqrt(Etilde) + fE= 1./numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5\ + *sqrtEtilde/(1-Etilde)**2.\ + *((1.-2.*Etilde)*(8.*Etilde**2.-8.*Etilde-3.)\ + +((3.*numpy.arcsin(sqrtEtilde))\ + /numpy.sqrt(Etilde*(1.-Etilde)))) elif self._beta == 0.5: - fE = (3*Etilde**2)/(4*(numpy.pi**3)*_GMa) + fE= (3.*Etilde**2.)/(4.*numpy.pi**3.*self._GMa) elif self._beta == -0.5: - fE = ((20*Etilde**3-20*Etilde**4+6*Etilde**5)\ - /(1-Etilde)**4)/(4*numpy.pi**3*(_GMa)**2) - elif self._use_BD02: - fE = self._fE_BD02(Etilde) - elif self._beta < 1.0 and self._beta > 0.5: - fE = self._fE_beta_gt05(Erel) - elif self._beta < 0.5 and self._beta > -0.5: - fE = self._fE_beta_gtm05_lt05(Erel) - if len(Etilde_out)>0: - fE[Etilde_out] = 0 - return fE - - def _fE_beta_gt05(self,Erel): - """Calculate fE for a Hernquist model when 0.5 < beta < 1.0""" - psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) - _a = self._pot.a - _GM = psi0*_a - Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self._beta)\ - /scipy.special.gamma(1.5-self._beta) - Cbeta = 2**(self._beta-0.5)/(2*numpy.pi*Ibeta) - alpha = self._beta-0.5 - coeff = (Cbeta*_a**(2*self._beta-2))*(numpy.sin(alpha*numpy.pi))\ - /(_GM*2*numpy.pi**2) - if hasattr(Erel,'shape'): - _Erel_shape = Erel.shape - _Erel_flat = Erel.flatten() - integral = numpy.zeros_like(_Erel_flat) - for ii in range(len(_Erel_flat)): - integral[ii] = scipy.integrate.quad( - self._fE_beta_gt05_integral, a=0, b=_Erel_flat[ii], - args=(_Erel_flat[ii],psi0))[0] + fE= ((20.*Etilde**3.-20.*Etilde**4.+6.*Etilde**5.)\ + /(1.-Etilde)**4)/(4.*numpy.pi**3.*self._GMa**2.) else: - integral = scipy.integrate.quad( - self._fE_beta_gt05_integral, a=0, b=Erel, - args=(Erel,psi0))[0] - return coeff*integral - - def _fE_beta_gt05_integral(self,psi,Erel,psi0): - """Integral for calculating fE for a Hernquist when 0.5 < beta < 1.0""" - psiTilde = psi/psi0 - # Absolute value because the answer normally comes out imaginary? - denom = numpy.abs( (Erel-psi)**(1.5-self._beta) ) - numer = ((4-2*self._beta)*numpy.power(1-psiTilde,2*self._beta-1)\ - *numpy.power(psiTilde,3-2*self._beta)+(1-2*self._beta)\ - *numpy.power(1-psiTilde,2*self._beta-2)\ - *numpy.power(psiTilde,4-2*self._beta)) - return numer/denom - - def _fE_beta_gtm05_lt05(self,Erel): - """Calculate fE for a Hernquist model when -0.5 < beta < 0.5""" - psi0 = -1*evaluatePotentials(self._pot,0,0,use_physical=False) - _a = self._pot.a - _GM = psi0*_a - alpha = 0.5-self._beta - Ibeta = numpy.sqrt(numpy.pi)*scipy.special.gamma(1-self._beta)\ - /scipy.special.gamma(1.5-self._beta) - Cbeta = 2**(self._beta-0.5)/(2*numpy.pi*Ibeta*alpha) - coeff = (Cbeta*_a**(2*self._beta-1))*(numpy.sin(alpha*numpy.pi))\ - /((_GM**2)*2*numpy.pi**2) - if hasattr(Erel,'shape'): - _Erel_shape = Erel.shape - _Erel_flat = Erel.flatten() - integral = numpy.zeros_like(_Erel_flat) - for ii in range(len(_Erel_flat)): - integral[ii] = scipy.integrate.quad( - self._fE_beta_gt05_integral, a=0, b=_Erel_flat[ii], - args=(_Erel_flat[ii],psi0))[0] - else: - integral = scipy.integrate.quad( - self._fE_beta_gt05_integral, a=0, b=Erel, - args=(Erel,psi0))[0] - return coeff*integral - - def _fE_beta_gtm05_lt05_integral(self,psi,Erel,psi0): - """Integral for calculating fE for a Hernquist when -0.5 < beta < 0.5""" - psiTilde = psi/psi0 - # Absolute value because the answer normally comes out imaginary? - denom = numpy.abs( (Erel-psi)**(0.5-self._beta) ) - numer = (4-2*self._beta-3*psiTilde)\ - *numpy.power(1-psiTilde,2*self._beta-2)\ - *numpy.power(psiTilde,3-2*self._beta) - return numer/denom - - def _fE_BD02(self,Erel): - """Calculate fE according to the hypergeometric solution of Baes & - Dejonghe (2002)""" - coeff = (2.**self._beta/(2.*numpy.pi)**2.5)*scipy.special.gamma(5.-2.*self._beta)/\ - ( scipy.special.gamma(1.-self._beta)*scipy.special.gamma(3.5-self._beta) ) - fE = coeff*numpy.power(Erel,2.5-self._beta)*\ - scipy.special.hyp2f1(5.-2.*self._beta,1.-2.*self._beta,3.5-self._beta,Erel) + fE= self._fEnorm*numpy.power(Etilde,2.5-self._beta)*\ + scipy.special.hyp2f1(5.-2.*self._beta,1.-2.*self._beta, + 3.5-self._beta,Etilde) + if len(Etilde_out) > 0: + fE[Etilde_out]= 0. return fE - + + def _icmf(self,ms): '''Analytic expression for the normalized inverse cumulative mass function. The argument ms is normalized mass fraction [0,1]''' diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index dcbe35fe3..f4554547d 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -101,7 +101,7 @@ def test_isotropic_hernquist_beta_directint(): ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) numpy.random.seed(10) @@ -124,7 +124,7 @@ def test_anisotropic_hernquist_dens_spherically_symmetric(): def test_anisotropic_hernquist_dens_massprofile(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) numpy.random.seed(10) @@ -137,7 +137,7 @@ def test_anisotropic_hernquist_dens_massprofile(): def test_anisotropic_hernquist_sigmar(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) numpy.random.seed(10) @@ -150,19 +150,19 @@ def test_anisotropic_hernquist_sigmar(): def test_anisotropic_hernquist_beta(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) numpy.random.seed(10) samp= dfh.sample(n=1000000) - tol= 8*1e-2 + tol= 8*1e-2 * (beta > -0.7) + 0.12 * (beta == -0.7) check_beta(samp,pot,tol,beta=beta, rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None def test_anisotropic_hernquist_meanvr_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) tol= 1e-8 @@ -172,7 +172,7 @@ def test_anisotropic_hernquist_meanvr_directint(): def test_anisotropic_hernquist_sigmar_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) tol= 1e-5 @@ -184,7 +184,7 @@ def test_anisotropic_hernquist_sigmar_directint(): def test_anisotropic_hernquist_beta_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) - betas= [-0.4,0.5] + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) tol= 1e-8 @@ -335,8 +335,8 @@ def check_beta(samp,pot,tol,beta=0., if not callable(beta): beta_func= lambda r: beta else: - beta_func= beta - assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value" + beta_func= beta + assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value for beta = {}".format(beta) return None def check_meanvr_directint(dfi,pot,tol,beta=0., From 3f07d3be023df6de0d298d5fcd862166fe1d9db7 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 17:49:52 -0400 Subject: [PATCH 65/91] Move call_internal to constantbetadf --- galpy/df/constantbetaHernquistdf.py | 31 +------------------------ galpy/df/constantbetadf.py | 35 ++++++++++++++++++++++------- galpy/df/sphericaldf.py | 2 +- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index d4e1dc14a..b8e1de6e6 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -40,36 +40,7 @@ def __init__(self,pot=None,beta=0,ro=None,vo=None): self._fEnorm= (2.**self._beta/(2.*numpy.pi)**2.5)\ *scipy.special.gamma(5.-2.*self._beta)/\ ( scipy.special.gamma(1.-self._beta)*scipy.special.gamma(3.5-self._beta) ) - - - - def _call_internal(self,*args): - """ - NAME: - - _call_internal - - PURPOSE: - - Evaluate the DF for a constant anisotropy Hernquist - - INPUT: - - E - The energy - - L - The angular momentum - - OUTPUT: - - fH - The value of the DF - - HISTORY: - - 2020-07-22 - Written - Lane (UofT) - """ - E, L= args - return L**(-2*self._beta)*self.fE(E) - + def fE(self,E): """ NAME: diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index e4b5dac61..6018ec279 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -3,7 +3,7 @@ import numpy import scipy.interpolate from scipy import integrate, special -from ..potential import evaluatePotentials, vesc +from ..potential import evaluatePotentials from .sphericaldf import anisotropicsphericaldf class constantbetadf(anisotropicsphericaldf): @@ -28,16 +28,35 @@ def __init__(self,pot=None,beta=None,scale=None,ro=None,vo=None): """ anisotropicsphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) - self._beta = beta - self._potInf = evaluatePotentials(pot,10**12,0) + self._beta= beta + self._potInf= evaluatePotentials(pot,10**12,0) def _call_internal(self,*args): - # Stub for calling - return None + """ + NAME: + + _call_internal + + PURPOSE: + + Evaluate the DF for a constant anisotropy Hernquist + + INPUT: - def fE(self,E): - # Stub for computing f_1(E) in BT08 nomenclature - return None + E - The energy + + L - The angular momentum + + OUTPUT: + + fH - The value of the DF + + HISTORY: + + 2020-07-22 - Written - Lane (UofT) + """ + E, L= args + return L**(-2*self._beta)*self.fE(E) def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 27b7092c0..6e46b0bf6 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -267,7 +267,7 @@ def _make_cmf_interpolator(self): so that xi is in the range [-1,1], which corresponds to an r range of [0,infinity)""" - xis = numpy.arange(-1,1,1e-6) + xis = numpy.arange(-1,1,1e-4) rs = _xiToR(xis,a=self._scale) ms = self._pot.mass(rs,use_physical=False) ms /= self._pot.mass(10**12,use_physical=False) From e356e23c3b1e2d38365f1b4638bc0e08de89401e Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 17:50:09 -0400 Subject: [PATCH 66/91] Add test of interpolating the inverse cumulative mass function --- tests/test_sphericaldf.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index f4554547d..698310e9d 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -40,6 +40,23 @@ def test_isotropic_hernquist_dens_massprofile(): tol,skip=1000) return None +def test_isotropic_hernquist_dens_massprofile_forcemassinterpolation(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + # Remove the inverse cumulative mass function to force its interpolation + class isotropicHernquistdfNoICMF(isotropicHernquistdf): + _icmf= property() + dfh= isotropicHernquistdfNoICMF(pot=pot) + print(hasattr(dfh,'_icmf')) + numpy.random.seed(10) + samp= dfh.sample(n=100000) + tol= 5*1e-3 + check_spherical_massprofile(samp, + lambda r: pot.mass(r)\ + /pot.mass(numpy.amax(samp.r()), + ), + tol,skip=1000) + return None + def test_isotropic_hernquist_sigmar(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) From af2b9dd261d4978fafe810d494ae41fd2cd6cd32 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 18:03:22 -0400 Subject: [PATCH 67/91] Test of errors when not supplying a Hernquist potential to the Hernquist DFs --- tests/test_sphericaldf.py | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 698310e9d..11c013baa 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -1,4 +1,5 @@ # Tests of spherical distribution functions +import pytest import numpy from scipy import special from galpy import potential @@ -285,6 +286,69 @@ def test_king_beta_directint(): rmin=dfk._scale/10.,rmax=dfk.rt*0.7,bins=31) return None +################################ TESTS OF ERRORS############################### + +def test_isotropic_hernquist_nopot(): + with pytest.raises(AssertionError) as excinfo: + dfh= isotropicHernquistdf() + assert str(excinfo.value) == 'pot= must be potential.HernquistPotential', 'Error message when not supplying the potential is incorrect' + return None + +def test_isotropic_hernquist_wrongpot(): + pot= potential.JaffePotential(amp=2.,a=1.3) + with pytest.raises(AssertionError) as excinfo: + dfh= isotropicHernquistdf(pot=pot) + assert str(excinfo.value) == 'pot= must be potential.HernquistPotential', 'Error message when not supplying the potential is incorrect' + return None + +def test_anisotropic_hernquist_nopot(): + with pytest.raises(AssertionError) as excinfo: + dfh= constantbetaHernquistdf() + assert str(excinfo.value) == 'pot= must be potential.HernquistPotential', 'Error message when not supplying the potential is incorrect' + return None + +def test_anisotropic_hernquist_wrongpot(): + pot= potential.JaffePotential(amp=2.,a=1.3) + with pytest.raises(AssertionError) as excinfo: + dfh= constantbetaHernquistdf(pot=pot) + assert str(excinfo.value) == 'pot= must be potential.HernquistPotential', 'Error message when not supplying the potential is incorrect' + return None + +############################# TESTS OF UNIT HANDLING########################### + +# Test that setting up a DF with unit conversion parameters that are +# incompatible with that of the underlying potential fails +def test_isotropic_hernquist_incompatibleunits(): + pot= potential.HernquistPotential(amp=2.,a=1.3,ro=9.,vo=210.) + with pytest.raises(RuntimeError): + dfh= isotropicHernquistdf(pot=pot,ro=8.,vo=210.) + with pytest.raises(RuntimeError): + dfh= isotropicHernquistdf(pot=pot,ro=9.,vo=230.) + with pytest.raises(RuntimeError): + dfh= isotropicHernquistdf(pot=pot,ro=8.,vo=230.) + return None + +# Test that the unit system is correctly transfered +def test_isotropic_hernquist_unittransfer(): + from galpy.util import conversion + ro, vo= 9., 210. + pot= potential.HernquistPotential(amp=2.,a=1.3,ro=ro,vo=vo) + dfh= isotropicHernquistdf(pot=pot) + phys= conversion.get_physical(dfh,include_set=True) + assert phys['roSet'], "sphericaldf's ro not set when that of the underlying potential is set" + assert phys['voSet'], "sphericaldf's vo not set when that of the underlying potential is set" + assert numpy.fabs(phys['ro']-ro) < 1e-8, "Potential's unit system not correctly transfered to sphericaldf's" + assert numpy.fabs(phys['vo']-vo) < 1e-8, "Potential's unit system not correctly transfered to sphericaldf's" + # Following should not be on + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + phys= conversion.get_physical(dfh,include_set=True) + assert not phys['roSet'], "sphericaldf's ro set when that of the underlying potential is not set" + assert not phys['voSet'], "sphericaldf's vo set when that of the underlying potential is not set" + return None + + + ############################### HELPER FUNCTIONS ############################## def check_spherical_symmetry(samp,l,m,tol): """Check for spherical symmetry by Monte Carlo integration of the From 736e7d8fa8fe8f065ca4f607cffa75db8fc40770 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 18:10:14 -0400 Subject: [PATCH 68/91] phi0 -> psi0 --- galpy/df/isotropicHernquistdf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index e1d8be0b6..02920b64f 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -10,8 +10,8 @@ class isotropicHernquistdf(Eddingtondf): def __init__(self,pot=None,ro=None,vo=None): assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' Eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) - self._phi0= -evaluatePotentials(self._pot,0,0,use_physical=False) - self._GMa = self._phi0*self._pot.a**2. + self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) + self._GMa = self._psi0*self._pot.a**2. self._fEnorm= 1./numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 def _call_internal(self,*args): @@ -61,7 +61,7 @@ def fE(self,E): 2020-08-09 - Written - James Lane (UofT) """ - Etilde= -conversion.parse_energy(E,vo=self._vo)/self._phi0 + Etilde= -conversion.parse_energy(E,vo=self._vo)/self._psi0 # Handle E out of bounds Etilde_out = numpy.where(numpy.logical_or(Etilde<0,Etilde>1))[0] if len(Etilde_out)>0: From 7b6525ac45c78b25e48e04125e113e7d865e95e8 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 18:10:25 -0400 Subject: [PATCH 69/91] Different scale handling in sphericaldf --- galpy/df/sphericaldf.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 6e46b0bf6..752fb3dc4 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -55,10 +55,8 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): try: self._scale = pot._scale except AttributeError: - if scale is not None: - self._scale= conversion.parse_length(scale,ro=self._ro) - else: - self._scale = 1. + self._scale= conversion.parse_length(scale,ro=self._ro) \ + if scale is not None else 1. ############################## EVALUATING THE DF############################### @physical_conversion('phasespacedensity',pop=True) From 6ab7876a6836ed22c6657e7e426dfd80681ad9c7 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 20:08:59 -0400 Subject: [PATCH 70/91] Test out-of-bounds behavior of (an)isotropic Hernquist DF --- galpy/df/constantbetadf.py | 2 +- galpy/df/sphericaldf.py | 8 +++++--- tests/test_sphericaldf.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index 6018ec279..d8186eb13 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -55,7 +55,7 @@ def _call_internal(self,*args): 2020-07-22 - Written - Lane (UofT) """ - E, L= args + E, L, _= args return L**(-2*self._beta)*self.fE(E) def _sample_eta(self,n=1): diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 752fb3dc4..b09ededaa 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -99,9 +99,11 @@ def __call__(self,*args,**kwargs): E = args[0].E(pot=self._pot,use_physical=False) L = numpy.sqrt(numpy.sum(args[0].L(use_physical=False)**2.)) Lz = args[0].Lz(use_physical=False) - E= conversion.parse_energy(E,vo=self._vo) - L= conversion.parse_angmom(L,ro=self._vo,vo=self._vo) - Lz= conversion.parse_angmom(Lz,ro=self._vo,vo=self._vo) + E= numpy.atleast_1d(conversion.parse_energy(E,vo=self._vo)) + L= numpy.atleast_1d(conversion.parse_angmom(L,ro=self._ro, + vo=self._vo)) + Lz= numpy.atleast_1d(conversion.parse_angmom(Lz,ro=self._vo, + vo=self._vo)) else: # Assume R,vR,vT,z,vz,(phi) R,vR,vT,z,vz, phi = (args+(None,))[:6] R= conversion.parse_length(R,ro=self._ro) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 11c013baa..7b5c861c5 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -116,6 +116,13 @@ def test_isotropic_hernquist_beta_directint(): bins=31) return None +def test_isotropic_hernquist_energyoutofbounds(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1,10.,0.1),))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E > 0 does not give zero' + assert numpy.all(numpy.fabs(dfh((pot(0,0)-1e-4,))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero' + return None + ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) @@ -212,6 +219,15 @@ def test_anisotropic_hernquist_beta_directint(): bins=31) return None +def test_anisotropic_hernquist_energyoutofbounds(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1,10.,0.1),1.1))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E > 0 does not give zero' + assert numpy.all(numpy.fabs(dfh((pot(0,0)-1e-4,1.1))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero' + return None + ################################# KING DF ##################################### def test_king_dens_spherically_symmetric(): dfk= kingdf(W0=3.,M=2.3,rt=1.76) From d5e9af69bdc8cd9244f608259b4dcb6841154565 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 20:38:41 -0400 Subject: [PATCH 71/91] Test more W0 cases of kingdf and also compare vmomentdensity to density directly --- galpy/df/sphericaldf.py | 2 +- tests/test_sphericaldf.py | 44 +++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index b09ededaa..b189723b1 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -443,7 +443,7 @@ def vmomentdensity(self,r,n,m): +0.5*v**2.), 0.,self._vmax_at_r(self._pot,r))[0]\ *special.gamma(m//2+1)*special.gamma(n//2+0.5)\ - /2./special.gamma(m//2+n//2+1.5) + /special.gamma(m//2+n//2+1.5) def _sample_eta(self,n=1): """Sample the angle eta which defines radial vs tangential velocities""" diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 7b5c861c5..2c882d7cb 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -261,17 +261,19 @@ def test_king_dens_massprofile(): return None def test_king_sigmar(): - pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) - dfk= kingdf(W0=3.,M=2.3,rt=1.76) - numpy.random.seed(10) - samp= dfk.sample(n=1000000) - # lower tolerance closer to rt because fewer stars there - tol= 0.07 - check_sigmar_against_jeans(samp,pot,tol,beta=0., - rmin=dfk._scale/10.,rmax=dfk.rt*0.7,bins=31) - tol= 0.2 - check_sigmar_against_jeans(samp,pot,tol,beta=0., - rmin=dfk.rt*0.8,rmax=dfk.rt,bins=5) + W0s= [1.,3.,9.] + for W0 in W0s: + pot= potential.KingPotential(W0=W0,M=2.3,rt=1.76) + dfk= kingdf(W0=W0,M=2.3,rt=1.76) + numpy.random.seed(10) + samp= dfk.sample(n=1000000) + # lower tolerance closer to rt because fewer stars there + tol= 0.09 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=dfk._scale/10.,rmax=dfk.rt*0.7,bins=31) + tol= 0.2 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=dfk.rt*0.8,rmax=dfk.rt*0.95,bins=5) return None def test_king_beta(): @@ -286,6 +288,15 @@ def test_king_beta(): bins=31) return None +def test_king_dens_directint(): + pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) + dfk= kingdf(W0=3.,M=2.3,rt=1.76) + tol= 0.02 + check_dens_directint(dfk,pot,tol,dfk.dens, + rmin=dfk._scale/10., + rmax=dfk.rt*0.7,bins=31) + return None + def test_king_sigmar_directint(): pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) dfk= kingdf(W0=3.,M=2.3,rt=1.76) @@ -436,6 +447,17 @@ def check_beta(samp,pot,tol,beta=0., assert numpy.all(numpy.fabs(samp_beta-beta_func(brs)) < tol), "beta(r) from samples does not agree with the expected value for beta = {}".format(beta) return None +def check_dens_directint(dfi,pot,tol,dens, + rmin=None,rmax=None,bins=31): + """Check that the density obtained from integrating over the DF agrees + with the expected density""" + rs= numpy.linspace(rmin,rmax,bins) + intdens= numpy.array([dfi.vmomentdensity(r,0,0) for r in rs]) + expdens= numpy.array([dens(r) for r in rs]) + assert numpy.all(numpy.fabs(intdens/expdens-1.) < tol), \ + "Density from direct integration is not equal to the expected value" + return None + def check_meanvr_directint(dfi,pot,tol,beta=0., rmin=None,rmax=None,bins=31): """Check that the mean v_r(r) obtained from integrating over the DF agrees From 082fcc7de7f341d2f5668213aac7f08941372606 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 20:42:08 -0400 Subject: [PATCH 72/91] No need to deal with vmax < vesc in building the p(v|r) grid any longer --- galpy/df/sphericaldf.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index b189723b1..6c9873425 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -357,14 +357,7 @@ def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, icdf_v_vesc_grid_reg = numpy.zeros((n_new_pvr,len(r_a_values))) for i in range(pvr_grid_cml_norm.shape[1]): cml_pvr = pvr_grid_cml_norm[:,i] - # Deal with the fact that the escape velocity might be beyond - # allowed velocities for the DF (e.g., King, where any start that - # escapes to r > rt is unbound, but v_esc is still defined by the - # escape to infinity) - try: - end_indx= numpy.amin(numpy.arange(len(cml_pvr))[cml_pvr == numpy.amax(cml_pvr)])+1 - except ValueError: - end_indx= len(cml_pvr) + end_indx= numpy.amin(numpy.arange(len(cml_pvr))[cml_pvr == numpy.amax(cml_pvr)])+1 cml_pvr_inv_interp = scipy.interpolate.InterpolatedUnivariateSpline(cml_pvr[:end_indx], v_vesc_values[:end_indx],k=3) pvr_samples_reg = numpy.linspace(0,1,n_new_pvr) From 3f374b7d954eadcb314a4195262a5a814d395cde Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:05:42 -0400 Subject: [PATCH 73/91] Also need theta when sampling at a single position --- galpy/df/sphericaldf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 6c9873425..b209dc7be 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -220,7 +220,11 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): """When R= is set to an array, z= needs to be set to """\ """an equal-length array""" n = len(R) - r = numpy.sqrt(R**2.+z**2.) + else: + R= R*numpy.ones(n) + z= z*numpy.ones(n) + r= numpy.sqrt(R**2.+z**2.) + theta= numpy.arctan2(R,z) if phi is None: # Otherwise assume phi input type matches R,z phi,_ = self._sample_position_angles(n=n) v = self._sample_v(r,n=n) From de7819784662119fa6dc3ae070461718770fa988 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:05:53 -0400 Subject: [PATCH 74/91] Tests of sampling a spherical DF at a single location --- tests/test_sphericaldf.py | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 2c882d7cb..bc1108151 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -7,6 +7,8 @@ from galpy.df import jeans ############################# ISOTROPIC HERNQUIST DF ########################## +# Note that we use the Hernquist case to check a bunch of code in the +# sphericaldf realm that doesn't need to be check for each new spherical DF def test_isotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -41,6 +43,51 @@ def test_isotropic_hernquist_dens_massprofile(): tol,skip=1000) return None +def test_isotropic_hernquist_singler_is_atsingler(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(R=1.3,z=0.,n=100000) + assert numpy.all(numpy.fabs(samp.r()-1.3) < 1e-8), 'Sampling a spherical distribution function at a single r does not produce orbits at a single r' + return None + +def test_isotropic_hernquist_singler_is_atrandomphi(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(R=1.3,z=0.,n=100000) + tol= 1e-2 + check_azimuthal_symmetry(samp,0,tol) + check_azimuthal_symmetry(samp,1,tol) + check_azimuthal_symmetry(samp,2,tol) + check_azimuthal_symmetry(samp,3,tol) + check_azimuthal_symmetry(samp,4,tol) + check_azimuthal_symmetry(samp,5,tol) + check_azimuthal_symmetry(samp,6,tol) + return None + +def test_isotropic_hernquist_singlerphi_is_atsinglephi(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp= dfh.sample(R=1.3,z=0.,phi=numpy.pi-0.3,n=100000) + assert numpy.all(numpy.fabs(samp.phi()-numpy.pi+0.3) < 1e-8), 'Sampling a spherical distribution function at a single r and phi oes not produce orbits at a single phi' + return None + +def test_isotropic_hernquist_givenr_are_atgivenr(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + r= numpy.linspace(0.1,10.,1001) + theta= numpy.random.uniform(size=len(r))*numpy.pi + # n should be ignored in the following + samp= dfh.sample(R=r*numpy.sin(theta),z=r*numpy.cos(theta),n=100000) + assert len(samp) == len(r), 'Length of sample with given r array is not equal to length of r' + assert numpy.all(numpy.fabs(samp.r()-r) < 1e-8), 'Sampling a spherical distribution function at given r does not produce orbits at these given r' + assert numpy.all(numpy.fabs(samp.R()-r*numpy.sin(theta)) < 1e-8), 'Sampling a spherical distribution function at given R does not produce orbits at these given R' + assert numpy.all(numpy.fabs(samp.z()-r*numpy.cos(theta)) < 1e-8), 'Sampling a spherical distribution function at given z does not produce orbits at these given z' + return None + def test_isotropic_hernquist_dens_massprofile_forcemassinterpolation(): pot= potential.HernquistPotential(amp=2.,a=1.3) # Remove the inverse cumulative mass function to force its interpolation @@ -68,6 +115,17 @@ def test_isotropic_hernquist_sigmar(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_isotropic_hernquist_singler_sigmar(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + for r in [0.3,1.3,2.3]: + samp= dfh.sample(R=r,z=0.,n=100000) + tol= 0.01 + check_sigmar_against_jeans(samp,pot,tol,beta=0., + rmin=r-0.1,rmax=r+0.1,bins=1) + return None + def test_isotropic_hernquist_beta(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -384,6 +442,13 @@ def check_spherical_symmetry(samp,l,m,tol): assert numpy.fabs(numpy.sum(special.lpmv(m,l,numpy.cos(thetas))*numpy.cos(m*phis))/samp.size-(l==0)*(m==0)) < tol, 'Sample does not appear to be spherically symmetric, fails spherical harmonics test for (l,m) = ({},{})'.format(l,m) return None +def check_azimuthal_symmetry(samp,m,tol): + """Check for spherical symmetry by Monte Carlo integration of the + spherical harmonic |Y_mn|^2 over the sample, should be zero unless l=m=0""" + thetas, phis= numpy.arctan2(samp.R(),samp.z()), samp.phi() + assert numpy.fabs(numpy.sum(numpy.cos(m*phis))/samp.size-(m==0)) < tol, 'Sample does not appear to be azimuthally symmetric, fails Fourier test for m = {}'.format(m) + return None + def check_spherical_massprofile(samp,mass_profile,tol,skip=100): """Check that the cumulative distribution of radii follows the cumulative mass profile (normalized such that total mass = 1)""" From 4184a1672990ef1325a1495535fce7bfdd172e4c Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:09:05 -0400 Subject: [PATCH 75/91] Also don't need the factor of two in constantbetadf's vmomentdensity --- galpy/df/constantbetadf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index d8186eb13..a95d01041 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -109,4 +109,4 @@ def vmomentdensity(self,r,n,m): +0.5*v**2.), 0.,self._vmax_at_r(self._pot,r))[0]\ *special.gamma(m/2.-self._beta+1.)*special.gamma((n+1)/2.)/\ - 2./special.gamma(0.5*(m+n-2.*self._beta+3.)) + special.gamma(0.5*(m+n-2.*self._beta+3.)) From daec37acd284df68b6f1e40ec64bdc6673794e9e Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:14:37 -0400 Subject: [PATCH 76/91] Test the units of the output orbits from sampling a spherical DF --- tests/test_sphericaldf.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index bc1108151..871d16531 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -432,7 +432,24 @@ def test_isotropic_hernquist_unittransfer(): assert not phys['voSet'], "sphericaldf's vo set when that of the underlying potential is not set" return None - +# Test that output orbits from sampling correctly have units on or off +def test_isotropic_hernquist_unitsofsamples(): + from galpy.util import conversion + ro, vo= 9., 210. + pot= potential.HernquistPotential(amp=2.,a=1.3,ro=ro,vo=vo) + dfh= isotropicHernquistdf(pot=pot) + samp= dfh.sample(n=100) + assert conversion.get_physical(samp,include_set=True)['roSet'], 'Orbit samples from spherical DF with units on do not have units on' + assert conversion.get_physical(samp,include_set=True)['voSet'], 'Orbit samples from spherical DF with units on do not have units on' + assert numpy.fabs(conversion.get_physical(samp,include_set=True)['ro']-ro) < 1e-8, 'Orbit samples from spherical DF with units on do not have correct ro' + assert numpy.fabs(conversion.get_physical(samp,include_set=True)['vo']-vo) < 1e-8, 'Orbit samples from spherical DF with units on do not have correct vo' + # Also test a case where they should be off + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + samp= dfh.sample(n=100) + assert not conversion.get_physical(samp,include_set=True)['roSet'], 'Orbit samples from spherical DF with units off do not have units off' + assert not conversion.get_physical(samp,include_set=True)['voSet'], 'Orbit samples from spherical DF with units off do not have units off' + return None ############################### HELPER FUNCTIONS ############################## def check_spherical_symmetry(samp,l,m,tol): From 7a22257d427dcd4718570fb666608da47c23a3b7 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:18:39 -0400 Subject: [PATCH 77/91] Test of R,vR,... output of spherical DF sampling --- tests/test_sphericaldf.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 871d16531..afe83e270 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -181,6 +181,23 @@ def test_isotropic_hernquist_energyoutofbounds(): assert numpy.all(numpy.fabs(dfh((pot(0,0)-1e-4,))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero' return None +# Check that samples of R,vR,.. are the same as orbit samples +def test_isotropic_hernquist_phasespacesamples_vs_orbitsamples(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + numpy.random.seed(10) + samp_orbits= dfh.sample(n=1000) + # Reset seed such that we should get the same + numpy.random.seed(10) + samp_RvR= dfh.sample(n=1000,return_orbit=False) + assert numpy.all(numpy.fabs(samp_orbits.R()-samp_RvR[0]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + assert numpy.all(numpy.fabs(samp_orbits.vR()-samp_RvR[1]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + assert numpy.all(numpy.fabs(samp_orbits.vT()-samp_RvR[2]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + assert numpy.all(numpy.fabs(samp_orbits.z()-samp_RvR[3]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + assert numpy.all(numpy.fabs(samp_orbits.vz()-samp_RvR[4]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + assert numpy.all(numpy.fabs(samp_orbits.phi()-samp_RvR[5]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' + return None + ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) From d15cf4a3d395d901fc880f10a0f68738f40164b8 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:31:03 -0400 Subject: [PATCH 78/91] Test the different ways of calling the ergodic and anisotropic DFs --- tests/test_sphericaldf.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index afe83e270..213aa2e34 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -198,6 +198,20 @@ def test_isotropic_hernquist_phasespacesamples_vs_orbitsamples(): assert numpy.all(numpy.fabs(samp_orbits.phi()-samp_RvR[5]) < 1e-8), 'Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits' return None +def test_isotropic_hernquist_diffcalls(): + from galpy.orbit import Orbit + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + # R,vR... vs. E + R,vR,vT,z,vz,phi= 1.1,0.3,0.2,0.9,-0.2,2.4 + # Calculate E directly + assert numpy.fabs(dfh(R,vR,vT,z,vz,phi)-dfh((pot(R,z)+0.5*(vR**2.+vT**2.+vz**2.),))) < 1e-8, 'Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer' + # Also L + assert numpy.fabs(dfh(R,vR,vT,z,vz,phi)-dfh((pot(R,z)+0.5*(vR**2.+vT**2.+vz**2.),numpy.sqrt(numpy.sum(Orbit([R,vR,vT,z,vz,phi]).L()**2.))))) < 1e-8, 'Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer' + # Also as orbit + assert numpy.fabs(dfh(R,vR,vT,z,vz,phi)-dfh(Orbit([R,vR,vT,z,vz,phi]))) < 1e-8, 'Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer' + return None + ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot= potential.HernquistPotential(amp=2.,a=1.3) @@ -303,6 +317,20 @@ def test_anisotropic_hernquist_energyoutofbounds(): assert numpy.all(numpy.fabs(dfh((pot(0,0)-1e-4,1.1))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero' return None +def test_anisotropic_hernquist_diffcalls(): + from galpy.orbit import Orbit + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + # R,vR... vs. E + R,vR,vT,z,vz,phi= 1.1,0.3,0.2,0.9,-0.2,2.4 + # Calculate E directly and L from Orbit + assert numpy.fabs(dfh(R,vR,vT,z,vz,phi)-dfh((pot(R,z)+0.5*(vR**2.+vT**2.+vz**2.),numpy.sqrt(numpy.sum(Orbit([R,vR,vT,z,vz,phi]).L()**2.))))) < 1e-8, 'Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer' + # Also as orbit + assert numpy.fabs(dfh(R,vR,vT,z,vz,phi)-dfh(Orbit([R,vR,vT,z,vz,phi]))) < 1e-8, 'Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer' + return None + ################################# KING DF ##################################### def test_king_dens_spherically_symmetric(): dfk= kingdf(W0=3.,M=2.3,rt=1.76) From 41734ceddb3bf042bea3e87bcafe1e495e7486ae Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:38:12 -0400 Subject: [PATCH 79/91] Test of initializing KingPotential with parameters with units --- tests/test_quantity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_quantity.py b/tests/test_quantity.py index 6c0176971..48d39c64a 100644 --- a/tests/test_quantity.py +++ b/tests/test_quantity.py @@ -3287,6 +3287,17 @@ def test_potential_paramunits(): # Check potential assert numpy.fabs(pot(4.,0.,phi=1.,use_physical=False)-pot_nounits(4.,0.,phi=1.,use_physical=False)) < 10.**-8., "TriaxialGaussianPotential w/ amp w/ units does not behave as expected" # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! + # KingPotential + pot= potential.KingPotential(W0=3.,M=4.*10.**6.*units.Msun, + rt=10.*units.pc, + ro=ro,vo=vo) + pot_nounits= potential.KingPotential(\ + W0=3.,M=4.*10.**6./conversion.mass_in_msol(vo,ro), + rt=10./1000/ro, + ro=ro,vo=vo) + # Check potential + assert numpy.fabs(pot(4.,0.,phi=1.,use_physical=False)-pot_nounits(4.,0.,phi=1.,use_physical=False)) < 10.**-8., "KingPotential w/ amp w/ units does not behave as expected" + # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! return None def test_potential_paramunits_2d(): From 40cb173a75881d5b4fa611a9926cbcf3cda2d863 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:43:12 -0400 Subject: [PATCH 80/91] Lowercase eddingtondf for more consistency(-ish...) --- galpy/df/Eddingtondf.py | 2 +- galpy/df/__init__.py | 4 ++-- galpy/df/isotropicHernquistdf.py | 6 +++--- galpy/df/sphericaldf.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/Eddingtondf.py index 026f6a619..b3d38c23a 100644 --- a/galpy/df/Eddingtondf.py +++ b/galpy/df/Eddingtondf.py @@ -3,7 +3,7 @@ from ..potential import evaluatePotentials from .sphericaldf import isotropicsphericaldf -class Eddingtondf(isotropicsphericaldf): +class eddingtondf(isotropicsphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula""" def __init__(self,pot=None,scale=None,ro=None,vo=None): """ diff --git a/galpy/df/__init__.py b/galpy/df/__init__.py index 610864340..a565eb04a 100644 --- a/galpy/df/__init__.py +++ b/galpy/df/__init__.py @@ -5,7 +5,7 @@ from . import streamdf from . import streamgapdf from . import jeans -from . import Eddingtondf +from . import eddingtondf from . import isotropicHernquistdf from . import constantbetaHernquistdf from . import kingdf @@ -36,7 +36,7 @@ quasiisothermaldf= quasiisothermaldf.quasiisothermaldf streamdf= streamdf.streamdf streamgapdf= streamgapdf.streamgapdf -Eddingtondf= Eddingtondf.Eddingtondf +eddingtondf= eddingtondf.eddingtondf isotropicHernquistdf= isotropicHernquistdf.isotropicHernquistdf constantbetaHernquistdf= constantbetaHernquistdf.constantbetaHernquistdf kingdf= kingdf.kingdf diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index 02920b64f..62a2c8821 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -3,13 +3,13 @@ import numpy from ..util import conversion from ..potential import evaluatePotentials,HernquistPotential -from .Eddingtondf import Eddingtondf +from .eddingtondf import eddingtondf -class isotropicHernquistdf(Eddingtondf): +class isotropicHernquistdf(eddingtondf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" def __init__(self,pot=None,ro=None,vo=None): assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' - Eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) + eddingtondf.__init__(self,pot=pot,ro=ro,vo=vo) self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) self._GMa = self._psi0*self._pot.a**2. self._fEnorm= 1./numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index b209dc7be..677e64f5d 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -49,7 +49,7 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): # they are compaible) if phys['roSet'] and phys['voSet']: self.turn_physical_on(ro=phys['ro'],vo=phys['vo']) - if pot is None: + if pot is None: # pragma: no cover raise IOError("pot= must be set") self._pot = pot try: From f417fcbe87ab31a42e6cb0b666929a73f127caa8 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 21:44:59 -0400 Subject: [PATCH 81/91] Actually rename Eddingtondf --> eddingtondf for conistency --- galpy/df/{Eddingtondf.py => eddingtondf.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename galpy/df/{Eddingtondf.py => eddingtondf.py} (100%) diff --git a/galpy/df/Eddingtondf.py b/galpy/df/eddingtondf.py similarity index 100% rename from galpy/df/Eddingtondf.py rename to galpy/df/eddingtondf.py From 31eec2d53db1efa5e232ec4cc3bfb547061c9254 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Wed, 9 Sep 2020 22:25:35 -0400 Subject: [PATCH 82/91] Add missing factor of r^-2beta in vmomentdensity of constantbetadf --- galpy/df/constantbetadf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index a95d01041..bcf15b2c6 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -102,7 +102,7 @@ def vmomentdensity(self,r,n,m): """ if m%2 == 1 or n%2 == 1: return 0. - return 2.*numpy.pi\ + return 2.*numpy.pi*r**(-2.*self._beta)\ *integrate.quad(lambda v: v**(2.-2.*self._beta+m+n) *self.fE(evaluatePotentials(self._pot,r,0, use_physical=False) From 5e7862471331a53ccf9a4c2d17f9e0a40f2a1e12 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:24:41 -0400 Subject: [PATCH 83/91] Add unit outputs for sphericaldf.vmomentdensity --- galpy/df/constantbetadf.py | 28 +---------- galpy/df/sphericaldf.py | 100 ++++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 72 deletions(-) diff --git a/galpy/df/constantbetadf.py b/galpy/df/constantbetadf.py index bcf15b2c6..46007808e 100644 --- a/galpy/df/constantbetadf.py +++ b/galpy/df/constantbetadf.py @@ -73,33 +73,7 @@ def _p_v_at_r(self,v,r): return self.fE(evaluatePotentials(self._pot,r,0,use_physical=False)\ +0.5*v**2.)*v**(2.-2.*self._beta) - def vmomentdensity(self,r,n,m): - """ - NAME: - - vmomentdensity - - PURPOSE: - - calculate the an arbitrary moment of the velocity distribution - at r times the density - - INPUT: - - r - spherical radius at which to calculate the moment - - n - vr^n, where vr = v x cos eta - - m - vt^m, where vt = v x sin eta - - OUTPUT: - - at r (no support for units) - - HISTORY: - - 2020-09-04 - Written - Bovy (UofT) - """ + def _vmomentdensity(self,r,n,m): if m%2 == 1 or n%2 == 1: return 0. return 2.*numpy.pi*r**(-2.*self._beta)\ diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 677e64f5d..999046716 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -11,6 +11,8 @@ from ..orbit import Orbit from ..util import conversion from ..util.conversion import physical_conversion +if conversion._APY_LOADED: + from astropy import units class sphericaldf(df): """Superclass for spherical distribution functions""" @@ -112,15 +114,17 @@ def __call__(self,*args,**kwargs): z= conversion.parse_length(z,ro=self._ro) vz= conversion.parse_velocity(vz,vo=self._vo) vtotSq = vR**2.+vT**2.+vz**2. - E= 0.5*vtotSq+evaluatePotentials(self._pot,R,z,use_physical=False) - Lz = R*vT + E= numpy.atleast_1d(0.5*vtotSq + +evaluatePotentials(self._pot,R,z, + use_physical=False)) + Lz = numpy.atleast_1d(R*vT) r = numpy.sqrt(R**2.+z**2.) vrad = (R*vR+z*vz)/r - L = numpy.sqrt(vtotSq-vrad**2.)*r + L = numpy.atleast_1d(numpy.sqrt(vtotSq-vrad**2.)*r) return self._call_internal(E,L,Lz) # Some function for each sub-class - def vmomentdensity(self,r,n,m): - """ + def vmomentdensity(self,r,n,m,**kwargs): + """ NAME: vmomentdensity @@ -145,24 +149,49 @@ def vmomentdensity(self,r,n,m): HISTORY: 2020-09-04 - Written - Bovy (UofT) - """ - return 2.*numpy.pi\ - *integrate.dblquad(lambda eta,v: v**(2.+m+n) - *numpy.sin(eta)**(1+m)*numpy.cos(eta)**n - *self(r,v*numpy.cos(eta),v*numpy.sin(eta),0.,0., - use_physical=False), - 0.,self._vmax_at_r(self._pot,r), - lambda x: 0.,lambda x: numpy.pi)[0] - + """ + r= conversion.parse_length(r,ro=self._ro) + use_physical= kwargs.pop('use_physical',True) + ro= kwargs.pop('ro',None) + if ro is None and hasattr(self,'_roSet') and self._roSet: + ro= self._ro + ro= conversion.parse_length_kpc(ro) + vo= kwargs.pop('vo',None) + if vo is None and hasattr(self,'_voSet') and self._voSet: + vo= self._vo + vo= conversion.parse_velocity_kms(vo) + if use_physical and not vo is None and not ro is None: + fac= vo**(n+m)/ro**3 + if conversion._APY_UNITS: + u= 1/units.kpc**3*(units.km/units.s)**(n+m) + out= self._vmomentdensity(r,n,m) + if conversion._APY_UNITS: + return units.Quantity(out*fac,unit=u) + else: + return out*fac + else: + return self._vmomentdensity(r,n,m) + + def _vmomentdensity(self,r,n,m): + return 2.*numpy.pi\ + *integrate.dblquad(lambda eta,v: v**(2.+m+n) + *numpy.sin(eta)**(1+m)*numpy.cos(eta)**n + *self(r,v*numpy.cos(eta),v*numpy.sin(eta),0.,0., + use_physical=False), + 0.,self._vmax_at_r(self._pot,r), + lambda x: 0.,lambda x: numpy.pi)[0] + @physical_conversion('velocity',pop=True) def sigmar(self,r): - return numpy.sqrt(self.vmomentdensity(r,2,0) - /self.vmomentdensity(r,0,0)) + r= conversion.parse_length(r,ro=self._ro) + return numpy.sqrt(self._vmomentdensity(r,2,0) + /self._vmomentdensity(r,0,0)) @physical_conversion('velocity',pop=True) def sigmat(self,r): - return numpy.sqrt(self.vmomentdensity(r,0,2) - /self.vmomentdensity(r,0,0)) + r= conversion.parse_length(r,ro=self._ro) + return numpy.sqrt(self._vmomentdensity(r,0,2) + /self._vmomentdensity(r,0,0)) def beta(self,r): return 1.-self.sigmat(r,use_physical=False)**2./2.\ @@ -215,6 +244,8 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): R = r*numpy.sin(theta) z = r*numpy.cos(theta) else: # 3D velocity samples + R= conversion.parse_length(R,ro=self._ro) + z= conversion.parse_length(z,ro=self._ro) if isinstance(R,numpy.ndarray): assert len(R) == len(z), \ """When R= is set to an array, z= needs to be set to """\ @@ -227,6 +258,11 @@ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True): theta= numpy.arctan2(R,z) if phi is None: # Otherwise assume phi input type matches R,z phi,_ = self._sample_position_angles(n=n) + else: + phi= conversion.parse_angle(phi) + phi= phi*numpy.ones(n) \ + if not hasattr(phi,'__len__') or len(phi) < n \ + else phi v = self._sample_v(r,n=n) eta,psi = self._sample_velocity_angles(n=n) vr = v*numpy.cos(eta) @@ -404,33 +440,7 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): """ sphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) - def vmomentdensity(self,r,n,m): - """ - NAME: - - vmomentdensity - - PURPOSE: - - calculate the an arbitrary moment of the velocity distribution - at r times the density - - INPUT: - - r - spherical radius at which to calculate the moment - - n - vr^n, where vr = v x cos eta - - m - vt^m, where vt = v x sin eta - - OUTPUT: - - at r (no support for units) - - HISTORY: - - 2020-09-04 - Written - Bovy (UofT) - """ + def _vmomentdensity(self,r,n,m): if m%2 == 1 or n%2 == 1: return 0. return 2.*numpy.pi\ From 78fb3a8eca6462e24254f8d8690259e4cba27ea2 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:24:56 -0400 Subject: [PATCH 84/91] Fix units of KingPotential used in kingdf --- galpy/df/kingdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index a7d2f3c64..cf2391d9c 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -65,7 +65,8 @@ def __init__(self,W0,M=1.,rt=1.,npt=1001,ro=None,vo=None): # Setup the potential, use original params in case they had units # because then the initialization will turn on units for this object from ..potential import KingPotential - pot= KingPotential(W0=self.W0,M=M,rt=rt,_sfkdf=self._scalefree_kdf) + pot= KingPotential(W0=self.W0,M=M,rt=rt,_sfkdf=self._scalefree_kdf, + ro=ro,vo=vo) # Now initialize the isotropic DF isotropicsphericaldf.__init__(self,pot=pot,scale=self.r0,ro=ro,vo=vo) self._potInf= self._pot(self.rt,0.,use_physical=False) From cbf57736ab20963c2553f682ddb5d905efbb4e85 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:25:07 -0400 Subject: [PATCH 85/91] Add a bunch of tests of quantity handling of the sphericaldfs --- tests/test_quantity.py | 159 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/tests/test_quantity.py b/tests/test_quantity.py index 48d39c64a..a094ca89d 100644 --- a/tests/test_quantity.py +++ b/tests/test_quantity.py @@ -5816,6 +5816,165 @@ def test_test_quasiisothermaldf_setup_refrloAsQuantity(): assert numpy.fabs(qdf._lo-10./vo/ro) < 10.**-10., 'lo in quasiisothermaldf setup as Quantity does not work as expected' return None +def test_sphericaldf_method_returntype(): + from galpy import potential + from galpy.df import isotropicHernquistdf, constantbetaHernquistdf + from galpy.orbit import Orbit + pot= potential.HernquistPotential(amp=2.,a=1.3,ro=8.,vo=220.) + dfh= isotropicHernquistdf(pot=pot) + dfa= constantbetaHernquistdf(pot=pot,beta=-0.2) + o= Orbit([1.1,0.1,1.1,0.1,0.03,0.4]) + assert isinstance(dfh(o),units.Quantity), 'sphericaldf method __call__ does not return Quantity when it should' + assert isinstance(dfh((o.E(pot=pot),)),units.Quantity), 'sphericaldf method __call__ does not return Quantity when it should' + assert isinstance(dfh(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi()),units.Quantity), 'sphericaldf method __call__ does not return Quantity when it should' + assert isinstance(dfh.vmomentdensity(1.1,0,0),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfa.vmomentdensity(1.1,0,0),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfh.vmomentdensity(1.1,1,0),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfa.vmomentdensity(1.1,1,0),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfh.vmomentdensity(1.1,0,2),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfa.vmomentdensity(1.1,0,2),units.Quantity), 'sphericaldf method vmomentdensity does not return Quantity when it should' + assert isinstance(dfh.sigmar(1.1),units.Quantity), 'sphericaldf method sigmar does not return Quantity when it should' + assert isinstance(dfh.sigmat(1.1),units.Quantity), 'sphericaldf method sigmar does not return Quantity when it should' + # beta should not be a quantity + assert not isinstance(dfh.beta(1.1),units.Quantity), "sphericaldf method beta returns Quantity when it shouldn't" + return None + +def test_sphericaldf_method_returnunit(): + from galpy import potential + from galpy.df import isotropicHernquistdf, constantbetaHernquistdf + from galpy.orbit import Orbit + pot= potential.HernquistPotential(amp=2.,a=1.3,ro=8.,vo=220.) + dfh= isotropicHernquistdf(pot=pot) + dfa= constantbetaHernquistdf(pot=pot,beta=-0.2) + o= Orbit([1.1,0.1,1.1,0.1,0.03,0.4]) + try: + dfh(o).to(1/units.kpc**3/(units.km/units.s)**3) + except units.UnitConversionError: + raise AssertionError('sphericaldf method __call__ does not return Quantity with the right units') + try: + dfh((o.E(pot=pot),)).to(1/units.kpc**3/(units.km/units.s)**3) + except units.UnitConversionError: + raise AssertionError('sphericaldf method __call__ does not return Quantity with the right units') + try: + dfh(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi()).to(1/units.kpc**3/(units.km/units.s)**3) + except units.UnitConversionError: + raise AssertionError('sphericaldf method __call__ does not return Quantity with the right units') + try: + dfh.vmomentdensity(1.1,0,0).to(1/units.kpc**3) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfa.vmomentdensity(1.1,0,0).to(1/units.kpc**3) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfh.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfa.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfh.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfa.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2) + except units.UnitConversionError: + raise AssertionError('sphericaldf method vmomentdensity does not return Quantity with the right units') + try: + dfh.sigmar(1.1).to(units.km/units.s) + except units.UnitConversionError: + raise AssertionError('sphericaldf method sigmar does not return Quantity with the right units') + try: + dfh.sigmat(1.1).to(units.km/units.s) + except units.UnitConversionError: + raise AssertionError('sphericaldf method sigmar does not return Quantity with the right units') + return None + +def test_sphericaldf_method_value(): + from galpy import potential + from galpy.df import isotropicHernquistdf, constantbetaHernquistdf + from galpy.orbit import Orbit + ro,vo= 8., 220. + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot,ro=ro,vo=vo) + dfh_nou= isotropicHernquistdf(pot=pot) + dfa= constantbetaHernquistdf(pot=pot,beta=-0.2,ro=ro,vo=vo) + dfa_nou= constantbetaHernquistdf(pot=pot,beta=-0.2) + o= Orbit([1.1,0.1,1.1,0.1,0.03,0.4]) + assert numpy.fabs(dfh(o).to(1/units.kpc**3/(units.km/units.s)**3).value-dfh_nou(o)/ro**3/vo**3) < 10.**-8., 'sphericaldf method __call__ does not return correct Quantity' + assert numpy.fabs(dfh((o.E(pot=pot),)).to(1/units.kpc**3/(units.km/units.s)**3).value-dfh_nou((o.E(pot=pot),))/ro**3/vo**3) < 10.**-8., 'sphericaldf method __call__ does not return correct Quantity' + assert numpy.fabs(dfh(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi()).to(1/units.kpc**3/(units.km/units.s)**3).value-dfh_nou(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi())/ro**3/vo**3) < 10.**-8., 'sphericaldf method __call__ does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1,0,0).to(1/units.kpc**3).value-dfh_nou.vmomentdensity(1.1,0,0)/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1,0,0).to(1/units.kpc**3).value-dfa_nou.vmomentdensity(1.1,0,0)/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s).value-dfh_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s).value-dfa_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfh_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfa_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.sigmar(1.1).to(units.km/units.s).value-dfh_nou.sigmar(1.1)*vo) < 10.**-8., 'sphericaldf method sigmar does not return correct Quantity' + assert numpy.fabs(dfh.sigmat(1.1).to(units.km/units.s).value-dfh_nou.sigmat(1.1)*vo) < 10.**-8., 'sphericaldf method sigmat does not return correct Quantity' + return None + +def test_sphericaldf_method_inputAsQuantity(): + from galpy import potential + from galpy.df import isotropicHernquistdf, constantbetaHernquistdf + from galpy.orbit import Orbit + ro,vo= 8., 220. + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot,ro=ro,vo=vo) + dfh_nou= isotropicHernquistdf(pot=pot) + dfa= constantbetaHernquistdf(pot=pot,beta=-0.2,ro=ro,vo=vo) + dfa_nou= constantbetaHernquistdf(pot=pot,beta=-0.2) + o= Orbit([1.1,0.1,1.1,0.1,0.03,0.4],ro=ro,vo=vo) + assert numpy.fabs(dfh((o.E(pot=pot),)).to(1/units.kpc**3/(units.km/units.s)**3).value-dfh_nou((o.E(pot=pot,use_physical=False),))/ro**3/vo**3) < 10.**-8., 'sphericaldf method __call__ does not return correct Quantity' + assert numpy.fabs(dfh(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi()).to(1/units.kpc**3/(units.km/units.s)**3).value-dfh_nou(o.R(use_physical=False),o.vR(use_physical=False),o.vT(use_physical=False),o.z(use_physical=False),o.vz(use_physical=False),o.phi(use_physical=False))/ro**3/vo**3) < 10.**-8., 'sphericaldf method __call__ does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1*ro*units.kpc,0,0).to(1/units.kpc**3).value-dfh_nou.vmomentdensity(1.1,0,0)/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1*ro*units.kpc,0,0,ro=ro*units.kpc).to(1/units.kpc**3).value-dfa_nou.vmomentdensity(1.1,0,0)/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1*ro*units.kpc,1,0,ro=ro,vo=vo*units.km/units.s).to(1/units.kpc**3*units.km/units.s).value-dfh_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1*ro*units.kpc,1,0,vo=vo*units.km/units.s).to(1/units.kpc**3*units.km/units.s).value-dfa_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.vmomentdensity(1.1*ro*units.kpc,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfh_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfa.vmomentdensity(1.1*ro*units.kpc,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfa_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + assert numpy.fabs(dfh.sigmar(1.1*ro*units.kpc).to(units.km/units.s).value-dfh_nou.sigmar(1.1)*vo) < 10.**-8., 'sphericaldf method sigmar does not return correct Quantity' + assert numpy.fabs(dfh.sigmat(1.1*ro*units.kpc).to(units.km/units.s).value-dfh_nou.sigmat(1.1)*vo) < 10.**-8., 'sphericaldf method sigmat does not return correct Quantity' + return None + +def test_sphericaldf_sample(): + from galpy import potential + from galpy.df import isotropicHernquistdf + from galpy.orbit import Orbit + ro,vo= 8., 220. + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot,ro=ro,vo=vo) + numpy.random.seed(10) + sam= dfh.sample(R=1.*units.kpc,z=0.*units.kpc,phi=10.*units.deg,n=2) + numpy.random.seed(10) + sam_nou= dfh.sample(R=1./ro,z=0./ro,phi=10./180.*numpy.pi,n=2) + assert numpy.all(numpy.fabs(sam.r(use_physical=False)-sam_nou.r(use_physical=False)) < 1e-8), 'Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units' + assert numpy.all(numpy.fabs(sam.vr(use_physical=False)-sam_nou.vr(use_physical=False)) < 1e-8), 'Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units' + # Array input + arr= numpy.array([1.,2.]) + numpy.random.seed(10) + sam= dfh.sample(R=arr*units.kpc,z=arr*0.*units.kpc, + phi=arr*10.*units.deg,n=len(arr)) + numpy.random.seed(10) + sam_nou= dfh.sample(R=arr/ro,z=arr*0./ro,phi=arr*10./180.*numpy.pi, + n=len(arr)) + assert numpy.all(numpy.fabs(sam.r(use_physical=False)-sam_nou.r(use_physical=False)) < 1e-8), 'Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units' + assert numpy.all(numpy.fabs(sam.vr(use_physical=False)-sam_nou.vr(use_physical=False)) < 1e-8), 'Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units' + return None + +def test_kingdf_setup_wunits(): + from galpy.util import conversion + from galpy.df import kingdf + ro, vo= 9., 210. + dfk= kingdf(W0=3.,M=4*1e4*units.Msun,rt=10.*units.pc,ro=ro,vo=vo) + dfk_nou= kingdf(W0=3.,M=4*1e4/conversion.mass_in_msol(vo,ro), + rt=10./ro/1000,ro=ro,vo=vo) + assert numpy.fabs(dfk.sigmar(1.*units.pc,use_physical=False)-dfk_nou.sigmar(1.*units.pc,use_physical=False)) < 1e-8, 'kingdf set up with parameters with units does not agree with kingdf not set up with parameters with units' + return None + def test_streamdf_method_returntype(): #Imports from galpy.df import streamdf From 04afc722e3778154b587fccad8efd1a0aefb3c49 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:31:09 -0400 Subject: [PATCH 86/91] Change kingdf normalization such that the DF is the number density --- galpy/df/kingdf.py | 2 +- tests/test_sphericaldf.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/galpy/df/kingdf.py b/galpy/df/kingdf.py index cf2391d9c..af77bd223 100644 --- a/galpy/df/kingdf.py +++ b/galpy/df/kingdf.py @@ -89,7 +89,7 @@ def fE(self,E): if numpy.sum(varE > 0.) > 0: out[varE > 0.]= (numpy.exp(varE[varE > 0.]/self._sigma2)-1.)\ *(2.*numpy.pi*self._sigma2)**-1.5*self.rho1 - return out + return out/self.M # number density def _vmax_at_r(self,pot,r): return numpy.sqrt(2.*(self._potInf-self._pot(r,0.,use_physical=False))) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 213aa2e34..201b8118d 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -395,7 +395,8 @@ def test_king_dens_directint(): pot= potential.KingPotential(W0=3.,M=2.3,rt=1.76) dfk= kingdf(W0=3.,M=2.3,rt=1.76) tol= 0.02 - check_dens_directint(dfk,pot,tol,dfk.dens, + check_dens_directint(dfk,pot,tol, + lambda r: dfk.dens(r)/2.3, # need to divide by mass rmin=dfk._scale/10., rmax=dfk.rt*0.7,bins=31) return None From 8f7d069962354317d8cd86dc575fdfd6332f2c89 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:37:11 -0400 Subject: [PATCH 87/91] Tests of the normalization of the isotropic and anisotropic Hernquist DFs (currently fails, because the anisotropic normalization isn't right) --- tests/test_sphericaldf.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 201b8118d..60bc2bf0d 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -136,6 +136,16 @@ def test_isotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None +def test_isotropic_hernquist_dens_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + dfh= isotropicHernquistdf(pot=pot) + tol= 1e-8 + check_dens_directint(dfh,pot,tol, + lambda r: pot.dens(r,0)/1., # need to divide by mass + rmin=pot._scale/10., + rmax=pot._scale*10.,bins=31) + return None + def test_isotropic_hernquist_meanvr_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) dfh= isotropicHernquistdf(pot=pot) @@ -273,7 +283,20 @@ def test_anisotropic_hernquist_beta(): check_beta(samp,pot,tol,beta=beta, rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None - + +@pytest.mark.xfail(raises=AssertionError,strict=True) +def test_anisotropic_hernquist_dens_directint(): + pot= potential.HernquistPotential(amp=2.,a=1.3) + betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] + for beta in betas: + dfh= constantbetaHernquistdf(pot=pot,beta=beta) + tol= 1e-8 + check_dens_directint(dfh,pot,tol, + lambda r: pot.dens(r,0)/1., # need to divide by mass + rmin=pot._scale/10., + rmax=pot._scale*10.,bins=31) + return None + def test_anisotropic_hernquist_meanvr_directint(): pot= potential.HernquistPotential(amp=2.,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] From d24cedc508d9184cbbfd1020cb7d552c27a7661c Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 21:48:46 -0400 Subject: [PATCH 88/91] Move _call_internal to general isotropicsphericaldf, add some notes about sphericaldfs --- galpy/df/isotropicHernquistdf.py | 25 ------------------- galpy/df/sphericaldf.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/galpy/df/isotropicHernquistdf.py b/galpy/df/isotropicHernquistdf.py index 62a2c8821..f65ad17e6 100644 --- a/galpy/df/isotropicHernquistdf.py +++ b/galpy/df/isotropicHernquistdf.py @@ -14,31 +14,6 @@ def __init__(self,pot=None,ro=None,vo=None): self._GMa = self._psi0*self._pot.a**2. self._fEnorm= 1./numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 - def _call_internal(self,*args): - """ - NAME: - - _call_internal - - PURPOSE - - Calculate the distribution function for an isotropic Hernquist - - INPUT: - - E,L,Lz - The energy, angular momemtum magnitude, and its z component (only E is used) - - OUTPUT: - - f(x,v) = f(E[x,v]) - - HISTORY: - - 2020-07 - Written - Lane (UofT) - - """ - return self.fE(args[0]) - def fE(self,E): """ NAME: diff --git a/galpy/df/sphericaldf.py b/galpy/df/sphericaldf.py index 999046716..ec364d16b 100644 --- a/galpy/df/sphericaldf.py +++ b/galpy/df/sphericaldf.py @@ -2,6 +2,24 @@ # - sphericaldf: superclass of all spherical DFs # - isotropicsphericaldf: superclass of all isotropic spherical DFs # - anisotropicsphericaldf: superclass of all anisotropic spherical DFs +# +# To implement a new DF do something like: +# - Inherit from isotropicsphericaldf for an isotropic DF and implement +# fE(self,E) which returns the DF as a function of E (see kingdf), then +# you should be set! You may also have to implement _vmax_at_r(self,pot,r) +# when the maximum velocity at a given position is less than the escape +# velocity +# - Inherit from anisotropicsphericaldf for an anisotropic DF, then you need +# to implement a bunch of functions: +# * _call_internal(self,*args,**kwargs): which returns the DF as a +# function of (E,L,Lz) +# * _sample_eta(self,n=1): to sample the velocity angle +# * _p_v_at_r(self,v,r): whcih returns p(v|r) +# constantbetadf is an example of this +# +# Note that we may have to re-think the implementation of anisotropic DFs to +# allow more general forms such as Osipkov-Merritt... +# import numpy import scipy.interpolate from scipy import integrate, special @@ -440,6 +458,31 @@ def __init__(self,pot=None,scale=None,ro=None,vo=None): """ sphericaldf.__init__(self,pot=pot,scale=scale,ro=ro,vo=vo) + def _call_internal(self,*args): + """ + NAME: + + _call_internal + + PURPOSE + + Calculate the distribution function for an isotropic DF + + INPUT: + + E,L,Lz - The energy, angular momemtum magnitude, and its z component (only E is used) + + OUTPUT: + + f(x,v) = f(E[x,v]) + + HISTORY: + + 2020-07 - Written - Lane (UofT) + + """ + return self.fE(args[0]) + def _vmomentdensity(self,r,n,m): if m%2 == 1 or n%2 == 1: return 0. From 5c84aaaaccfa86ce0fb55d3154d14416ad56db97 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Thu, 10 Sep 2020 22:23:26 -0400 Subject: [PATCH 89/91] Fix forcevmoment test for new vmoment implementation and make sure to hit non-quantity output --- tests/test_quantity.py | 5 +++++ tests/test_sphericaldf.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_quantity.py b/tests/test_quantity.py index a094ca89d..748275535 100644 --- a/tests/test_quantity.py +++ b/tests/test_quantity.py @@ -5911,6 +5911,11 @@ def test_sphericaldf_method_value(): assert numpy.fabs(dfa.vmomentdensity(1.1,0,0).to(1/units.kpc**3).value-dfa_nou.vmomentdensity(1.1,0,0)/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' assert numpy.fabs(dfh.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s).value-dfh_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' assert numpy.fabs(dfa.vmomentdensity(1.1,1,0).to(1/units.kpc**3*units.km/units.s).value-dfa_nou.vmomentdensity(1.1,1,0)*vo/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + # One with no quantity output + from galpy.util import conversion + conversion._APY_UNITS= False # Hack + assert numpy.fabs(dfh.vmomentdensity(1.1,0,2)-dfh_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' + conversion._APY_UNITS= True # Hack assert numpy.fabs(dfh.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfh_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' assert numpy.fabs(dfa.vmomentdensity(1.1,0,2).to(1/units.kpc**3*units.km**2/units.s**2).value-dfa_nou.vmomentdensity(1.1,0,2)*vo**2/ro**3) < 10.**-8., 'sphericaldf method vmomentdensity does not return correct Quantity' assert numpy.fabs(dfh.sigmar(1.1).to(units.km/units.s).value-dfh_nou.sigmar(1.1)*vo) < 10.**-8., 'sphericaldf method sigmar does not return correct Quantity' diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 60bc2bf0d..90fc81b5a 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -639,8 +639,8 @@ def check_sigmar_against_jeans_directint_forcevmoment(dfi,pot,tol,beta=0., class' vmomentdensity""" from galpy.df.sphericaldf import sphericaldf rs= numpy.linspace(rmin,rmax,bins) - intsr= numpy.array([numpy.sqrt(sphericaldf.vmomentdensity(dfi,r,2,0)/ - sphericaldf.vmomentdensity(dfi,r,0,0)) + intsr= numpy.array([numpy.sqrt(sphericaldf._vmomentdensity(dfi,r,2,0)/ + sphericaldf._vmomentdensity(dfi,r,0,0)) for r in rs]) jeanssr= numpy.array([jeans.sigmar(pot,r,beta=beta,use_physical=False) for r in rs]) assert numpy.all(numpy.fabs(intsr/jeanssr-1) < tol), \ From 9fd368564f76fea4b48ddfe0b84c6676253eafeb Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 11 Sep 2020 18:06:22 -0400 Subject: [PATCH 90/91] Fix normalization of Baes & Dejonghe --- galpy/df/constantbetaHernquistdf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/galpy/df/constantbetaHernquistdf.py b/galpy/df/constantbetaHernquistdf.py index b8e1de6e6..c7b7bdf52 100644 --- a/galpy/df/constantbetaHernquistdf.py +++ b/galpy/df/constantbetaHernquistdf.py @@ -38,8 +38,10 @@ def __init__(self,pot=None,beta=0,ro=None,vo=None): self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) self._GMa = self._psi0*self._pot.a**2. self._fEnorm= (2.**self._beta/(2.*numpy.pi)**2.5)\ - *scipy.special.gamma(5.-2.*self._beta)/\ - ( scipy.special.gamma(1.-self._beta)*scipy.special.gamma(3.5-self._beta) ) + *scipy.special.gamma(5.-2.*self._beta)\ + /scipy.special.gamma(1.-self._beta)\ + /scipy.special.gamma(3.5-self._beta)\ + /self._GMa**(1.5-self._beta) def fE(self,E): """ From 64845f4749b10545e16b03d685b34ad8a478c889 Mon Sep 17 00:00:00 2001 From: Jo Bovy Date: Fri, 11 Sep 2020 18:06:43 -0400 Subject: [PATCH 91/91] Make sure to use a Hernquist mass that's not 1, remove expected failure --- tests/test_sphericaldf.py | 64 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/tests/test_sphericaldf.py b/tests/test_sphericaldf.py index 90fc81b5a..e1bab70df 100644 --- a/tests/test_sphericaldf.py +++ b/tests/test_sphericaldf.py @@ -10,7 +10,7 @@ # Note that we use the Hernquist case to check a bunch of code in the # sphericaldf realm that doesn't need to be check for each new spherical DF def test_isotropic_hernquist_dens_spherically_symmetric(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(n=100000) @@ -31,7 +31,7 @@ def test_isotropic_hernquist_dens_spherically_symmetric(): return None def test_isotropic_hernquist_dens_massprofile(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(n=100000) @@ -44,7 +44,7 @@ def test_isotropic_hernquist_dens_massprofile(): return None def test_isotropic_hernquist_singler_is_atsingler(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(R=1.3,z=0.,n=100000) @@ -52,7 +52,7 @@ def test_isotropic_hernquist_singler_is_atsingler(): return None def test_isotropic_hernquist_singler_is_atrandomphi(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(R=1.3,z=0.,n=100000) @@ -67,7 +67,7 @@ def test_isotropic_hernquist_singler_is_atrandomphi(): return None def test_isotropic_hernquist_singlerphi_is_atsinglephi(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(R=1.3,z=0.,phi=numpy.pi-0.3,n=100000) @@ -75,7 +75,7 @@ def test_isotropic_hernquist_singlerphi_is_atsinglephi(): return None def test_isotropic_hernquist_givenr_are_atgivenr(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) r= numpy.linspace(0.1,10.,1001) @@ -89,12 +89,11 @@ def test_isotropic_hernquist_givenr_are_atgivenr(): return None def test_isotropic_hernquist_dens_massprofile_forcemassinterpolation(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) # Remove the inverse cumulative mass function to force its interpolation class isotropicHernquistdfNoICMF(isotropicHernquistdf): _icmf= property() dfh= isotropicHernquistdfNoICMF(pot=pot) - print(hasattr(dfh,'_icmf')) numpy.random.seed(10) samp= dfh.sample(n=100000) tol= 5*1e-3 @@ -106,7 +105,7 @@ class isotropicHernquistdfNoICMF(isotropicHernquistdf): return None def test_isotropic_hernquist_sigmar(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(n=100000) @@ -116,7 +115,7 @@ def test_isotropic_hernquist_sigmar(): return None def test_isotropic_hernquist_singler_sigmar(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) for r in [0.3,1.3,2.3]: @@ -127,7 +126,7 @@ def test_isotropic_hernquist_singler_sigmar(): return None def test_isotropic_hernquist_beta(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp= dfh.sample(n=1000000) @@ -137,17 +136,17 @@ def test_isotropic_hernquist_beta(): return None def test_isotropic_hernquist_dens_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) tol= 1e-8 check_dens_directint(dfh,pot,tol, - lambda r: pot.dens(r,0)/1., # need to divide by mass + lambda r: pot.dens(r,0)/(2.3/2.), # need to divide by mass rmin=pot._scale/10., rmax=pot._scale*10.,bins=31) return None def test_isotropic_hernquist_meanvr_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) tol= 1e-8 check_meanvr_directint(dfh,pot,tol,beta=0.,rmin=pot._scale/10., @@ -155,7 +154,7 @@ def test_isotropic_hernquist_meanvr_directint(): return None def test_isotropic_hernquist_sigmar_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) tol= 1e-5 check_sigmar_against_jeans_directint(dfh,pot,tol,beta=0., @@ -165,7 +164,7 @@ def test_isotropic_hernquist_sigmar_directint(): return None def test_isotropic_hernquist_sigmar_directint_forcevmoment(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) tol= 1e-5 check_sigmar_against_jeans_directint_forcevmoment(dfh,pot,tol,beta=0., @@ -175,7 +174,7 @@ def test_isotropic_hernquist_sigmar_directint_forcevmoment(): return None def test_isotropic_hernquist_beta_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) tol= 1e-8 check_beta_directint(dfh,tol,beta=0., @@ -185,7 +184,7 @@ def test_isotropic_hernquist_beta_directint(): return None def test_isotropic_hernquist_energyoutofbounds(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1,10.,0.1),))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E > 0 does not give zero' assert numpy.all(numpy.fabs(dfh((pot(0,0)-1e-4,))) < 1e-8), 'Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero' @@ -193,7 +192,7 @@ def test_isotropic_hernquist_energyoutofbounds(): # Check that samples of R,vR,.. are the same as orbit samples def test_isotropic_hernquist_phasespacesamples_vs_orbitsamples(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp_orbits= dfh.sample(n=1000) @@ -210,7 +209,7 @@ def test_isotropic_hernquist_phasespacesamples_vs_orbitsamples(): def test_isotropic_hernquist_diffcalls(): from galpy.orbit import Orbit - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) dfh= isotropicHernquistdf(pot=pot) # R,vR... vs. E R,vR,vT,z,vz,phi= 1.1,0.3,0.2,0.9,-0.2,2.4 @@ -224,7 +223,7 @@ def test_isotropic_hernquist_diffcalls(): ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -247,7 +246,7 @@ def test_anisotropic_hernquist_dens_spherically_symmetric(): return None def test_anisotropic_hernquist_dens_massprofile(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -260,7 +259,7 @@ def test_anisotropic_hernquist_dens_massprofile(): return None def test_anisotropic_hernquist_sigmar(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -273,7 +272,7 @@ def test_anisotropic_hernquist_sigmar(): return None def test_anisotropic_hernquist_beta(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -284,21 +283,20 @@ def test_anisotropic_hernquist_beta(): rmin=pot._scale/10.,rmax=pot._scale*10.,bins=31) return None -@pytest.mark.xfail(raises=AssertionError,strict=True) def test_anisotropic_hernquist_dens_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) - tol= 1e-8 + tol= 1e-7 check_dens_directint(dfh,pot,tol, - lambda r: pot.dens(r,0)/1., # need to divide by mass + lambda r: pot.dens(r,0)/(2.3/2.), # need to divide by mass rmin=pot._scale/10., rmax=pot._scale*10.,bins=31) return None def test_anisotropic_hernquist_meanvr_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -308,7 +306,7 @@ def test_anisotropic_hernquist_meanvr_directint(): return None def test_anisotropic_hernquist_sigmar_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -320,7 +318,7 @@ def test_anisotropic_hernquist_sigmar_directint(): return None def test_anisotropic_hernquist_beta_directint(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -332,7 +330,7 @@ def test_anisotropic_hernquist_beta_directint(): return None def test_anisotropic_hernquist_energyoutofbounds(): - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta) @@ -342,7 +340,7 @@ def test_anisotropic_hernquist_energyoutofbounds(): def test_anisotropic_hernquist_diffcalls(): from galpy.orbit import Orbit - pot= potential.HernquistPotential(amp=2.,a=1.3) + pot= potential.HernquistPotential(amp=2.3,a=1.3) betas= [-0.7,-0.5,-0.4,0.,0.3,0.5] for beta in betas: dfh= constantbetaHernquistdf(pot=pot,beta=beta)