Source code for galpy.df.eddingtondf

# Class that implements isotropic spherical DFs computed using the Eddington
# formula
import numpy
from scipy import interpolate, integrate
from ..util import conversion
from ..potential import evaluateR2derivs
from ..potential.Potential import _evaluatePotentials, _evaluateRforces
from .sphericaldf import isotropicsphericaldf, sphericaldf

[docs]class eddingtondf(isotropicsphericaldf): """Class that implements isotropic spherical DFs computed using the Eddington formula .. math:: f(\\mathcal{E}) = \\frac{1}{\\sqrt{8}\\,\\pi^2}\\,\\left[\\int_0^\\mathcal{E}\\mathrm{d}\\Psi\\,\\frac{1}{\\sqrt{\\mathcal{E}-\\Psi}}\\,\\frac{\\mathrm{d}^2\\rho}{\\mathrm{d}\\Psi^2} +\\frac{1}{\\sqrt{\\mathcal{E}}}\\,\\frac{\\mathrm{d}\\rho}{\\mathrm{d}\\Psi}\\Bigg|_{\\Psi=0}\\right]\\,, where :math:`\\Psi = -\\Phi+\\Phi(\\infty)` is the relative potential, :math:`\\mathcal{E} = \\Psi-v^2/2` is the relative (binding) energy, and :math:`\\rho` is the density of the tracer population (not necessarily the density corresponding to :math:`\\Psi` according to the Poisson equation). Note that the second term on the right-hand side is currently assumed to be zero in the code."""
[docs] def __init__(self,pot=None,denspot=None,rmax=1e4, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an isotropic distribution function computed using the Eddington inversion INPUT: pot= (None) Potential instance or list thereof that represents the gravitational potential (assumed to be spherical) denspot= (None) Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot) rmax= (None) maximum radius to consider (can be Quantity); DF is cut off at E = Phi(rmax) scale= Characteristic scale radius to aid sampling calculations. Optionaland will also be overridden by value from pot if available. ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2021-02-04 - Written - Bovy (UofT) """ isotropicsphericaldf.__init__(self,pot=pot,denspot=denspot, rmax=rmax,scale=scale,ro=ro,vo=vo) self._dnudr= self._denspot._ddensdr \ if not isinstance(self._denspot,list) \ else lambda r: numpy.sum([p._ddensdr(r) for p in self._denspot]) self._d2nudr2= self._denspot._d2densdr2 \ if not isinstance(self._denspot,list) \ else lambda r: numpy.sum([p._d2densdr2(r) for p in self._denspot]) self._potInf= _evaluatePotentials(pot,self._rmax,0) self._Emin= _evaluatePotentials(pot,0.,0) # Build interpolator r(pot) r_a_values= numpy.concatenate(\ (numpy.array([0.]), numpy.geomspace(1e-6,1e6,10001))) self._rphi= interpolate.InterpolatedUnivariateSpline(\ [_evaluatePotentials(self._pot,r*self._scale,0) for r in r_a_values],r_a_values*self._scale,k=3)
def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True,rmin=0.): # Slight over-write of superclass method to first build f(E) interp # No docstring so superclass' is used if not hasattr(self,'_fE_interp'): Es4interp= numpy.hstack((numpy.geomspace(1e-8,0.5,101, endpoint=False), sorted(1.-numpy.geomspace(1e-4,0.5,101)))) Es4interp= (Es4interp*(self._Emin-self._potInf)+self._potInf)[::-1] fE4interp= self.fE(Es4interp) iindx= numpy.isfinite(fE4interp) self._fE_interp= interpolate.InterpolatedUnivariateSpline(\ Es4interp[iindx],fE4interp[iindx], k=3,ext=3) return sphericaldf.sample(self,R=R,z=z,phi=phi,n=n, return_orbit=return_orbit,rmin=rmin) def fE(self,E): """ NAME: fE PURPOSE Calculate the energy portion of a DF computed using the Eddington inversion INPUT: E - The energy (can be Quantity) OUTPUT: fE - The value of the energy portion of the DF HISTORY: 2021-02-04 - Written - Bovy (UofT) """ Eint= conversion.parse_energy(E,vo=self._vo) out= numpy.zeros_like(Eint) indx= (Eint < self._potInf)*(Eint >= self._Emin) # Split integral at twice the lower limit to deal with divergence at # the lower end and infinity at the upper end out[indx]= numpy.array([integrate.quad( lambda t: _fEintegrand_smallr(t,self._pot,tE, self._dnudr,self._d2nudr2, self._rphi(tE)), 0.,numpy.sqrt(self._rphi(tE)), points=[0.])[0] for tE in Eint[indx]]) out[indx]+= numpy.array([integrate.quad( lambda t: _fEintegrand_larger(t,self._pot,tE, self._dnudr,self._d2nudr2), 0.,0.5/self._rphi(tE))[0] for tE in Eint[indx]]) return -out/(numpy.sqrt(8.)*numpy.pi**2.)
def _fEintegrand_raw(r,pot,E,dnudr,d2nudr2): # The 'raw', i.e., direct integrand in the Eddington inversion Fr= _evaluateRforces(pot,r,0) return (Fr*d2nudr2(r) +dnudr(r)*evaluateR2derivs(pot,r,0,use_physical=False))\ /Fr**2.\ /numpy.sqrt(_evaluatePotentials(pot,r,0)-E) def _fEintegrand_smallr(t,pot,E,dnudr,d2nudr2,rmin): # The integrand at small r, using transformation to deal with sqrt diverge return 2.*t*_fEintegrand_raw(t**2.+rmin,pot,E,dnudr,d2nudr2) def _fEintegrand_larger(t,pot,E,dnudr,d2nudr2): # The integrand at large r, using transformation to deal with infinity return 1./t**2*_fEintegrand_raw(1./t,pot,E,dnudr,d2nudr2)