Source code for galpy.df.osipkovmerrittdf

# Class that implements anisotropic DFs of the Osipkov-Merritt type
import numpy
from scipy import integrate, special, interpolate
from ..util import conversion
from ..potential import evaluateDensities
from ..potential.Potential import _evaluatePotentials
from .sphericaldf import anisotropicsphericaldf, sphericaldf
from .eddingtondf import eddingtondf

# This is the general Osipkov-Merritt superclass, implementation of general
# formula can be found following this class
class _osipkovmerrittdf(anisotropicsphericaldf):
    """General Osipkov-Merritt superclass with useful functions for any DF of the Osipkov-Merritt type."""
    def __init__(self,pot=None,denspot=None,ra=1.4,rmax=None,
                 scale=None,ro=None,vo=None):
        """
        NAME:

            __init__

        PURPOSE:

            Initialize a DF with Osipkov-Merritt anisotropy

        INPUT:

            pot= (None) Potential instance or list thereof

            denspot= (None) Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot)

            ra - anisotropy radius (can be a Quantity)

           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. 
                Not necessary, and will also be overridden by value from pot 
                if available.

           ro=, vo= galpy unit parameters

        OUTPUT:

            None

        HISTORY:

            2020-11-12 - Written - Bovy (UofT)

        """
        anisotropicsphericaldf.__init__(self,pot=pot,denspot=denspot,
                                        rmax=rmax,scale=scale,ro=ro,vo=vo)
        self._ra= conversion.parse_length(ra,ro=self._ro)
        self._ra2= self._ra**2.

    def _call_internal(self,*args):
        """
        NAME:

            _call_internal

        PURPOSE:

            Evaluate the DF for an Osipkov-Merritt-anisotropy DF

        INPUT:

            E - The energy

            L - The angular momentum

        OUTPUT:

            fH - The value of the DF

        HISTORY:

            2020-11-12 - Written - Bovy (UofT)

        """
        E, L, _= args
        return self.fQ(-E-0.5*L**2./self._ra2)

    def _sample_eta(self,r,n=1):
        """Sample the angle eta which defines radial vs tangential velocities"""
        # cumulative distribution of x = cos eta satisfies
        # x/(sqrt(A+1 -A* x^2)) = 2 b - 1 = c
        # where b \in [0,1] and A = (r/ra)^2
        # Solved by
        # x = c sqrt(1+[r/ra]^2) / sqrt( [r/ra]^2 c^2 + 1 ) for c > 0 [b > 0.5]
        # and symmetric wrt c
        c= numpy.random.uniform(size=n)
        x= c*numpy.sqrt(1+r**2./self._ra2)/numpy.sqrt(r**2./self._ra2*c**2.+1)
        x*= numpy.random.choice([1.,-1.],size=n)
        return numpy.arccos(x)

    def _p_v_at_r(self,v,r):
        """p( v*sqrt[1+r^2/ra^2*sin^2eta] | r) used in sampling """
        if hasattr(self,'_logfQ_interp'):
            return numpy.exp(\
                    self._logfQ_interp(-_evaluatePotentials(self._pot,r,0)\
                                   -0.5*v**2.))*v**2.
        else:
            return self.fQ(-_evaluatePotentials(self._pot,r,0)\
                           -0.5*v**2.)*v**2.

    def _sample_v(self,r,eta,n=1):
        """Generate velocity samples"""
        # Use super-class method to obtain v*[1+r^2/ra^2*sin^2eta]
        out= super(_osipkovmerrittdf,self)._sample_v(r,eta,n=n)
        # Transform to v
        return out/numpy.sqrt(1.+r**2./self._ra2*numpy.sin(eta)**2.)

    def _vmomentdensity(self,r,n,m):
         if m%2 == 1 or n%2 == 1:
             return 0.
         return 2.*numpy.pi*integrate.quad(lambda v: v**(2.+m+n)
                                *self.fQ(-_evaluatePotentials(self._pot,r,0)
                                             -0.5*v**2.),
                             0.,self._vmax_at_r(self._pot,r))[0]\
            *special.gamma(m/2.+1.)*special.gamma((n+1)/2.)/\
            special.gamma(0.5*(m+n+3.))/(1+r**2./self._ra2)**(m/2+1)
    
[docs]class osipkovmerrittdf(_osipkovmerrittdf): """Class that implements spherical DFs with Osipkov-Merritt-type orbital anisotropy .. math:: \\beta(r) = \\frac{1}{1+r_a^2/r^2} with :math:`r_a` the anistropy radius for arbitrary combinations of potential and density profile."""
[docs] def __init__(self,pot=None,denspot=None,ra=1.4,rmax=1e4, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a DF with Osipkov-Merritt anisotropy INPUT: pot= (None) Potential instance or list thereof denspot= (None) Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot) ra - anisotropy radius (can be a Quantity) rmax= (1e4) 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-07 - Written - Bovy (UofT) """ _osipkovmerrittdf.__init__(self,pot=pot,denspot=denspot,ra=ra, rmax=rmax,scale=scale,ro=ro,vo=vo) # Because f(Q) is the same integral as the Eddington conversion, but # using the augmented density rawdensx(1+r^2/ra^2), we use a helper # eddingtondf to do this integral, hacked to use the augmented density self._edf= eddingtondf(pot=self._pot,denspot=self._denspot,scale=scale, rmax=rmax,ro=ro,vo=vo) self._edf._dnudr= \ (lambda r: self._denspot._ddensdr(r)*(1.+r**2./self._ra2) \ +2.*self._denspot.dens(r,0,use_physical=False)*r/self._ra2)\ if not isinstance(self._denspot,list) \ else (lambda r: numpy.sum([p._ddensdr(r) for p in self._denspot])\ *(1.+r**2./self._ra2)\ +2.*evaluateDensities(self._denspot,r,0,use_physical=False)\ *r/self._ra2) self._edf._d2nudr2= \ (lambda r: self._denspot._d2densdr2(r)*(1.+r**2./self._ra2) \ +4.*self._denspot._ddensdr(r)*r/self._ra2 \ +2.*self._denspot.dens(r,0,use_physical=False)/self._ra2)\ if not isinstance(self._denspot,list) \ else (lambda r: numpy.sum([p._d2densdr2(r) for p in self._denspot])\ *(1.+r**2./self._ra2)\ +4.*numpy.sum([p._ddensdr(r) for p in self._denspot])\ *r/self._ra2 \ +2.*evaluateDensities(self._denspot,r,0,use_physical=False)\ /self._ra2)
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(Q) interp # No docstring so superclass' is used if not hasattr(self,'_logfQ_interp'): Qs4interp= numpy.hstack((numpy.geomspace(1e-8,0.5,101, endpoint=False), sorted(1.-numpy.geomspace(1e-8,0.5,101)))) Qs4interp= -(Qs4interp*(self._edf._Emin-self._edf._potInf) +self._edf._potInf) fQ4interp= numpy.log(self.fQ(Qs4interp)) iindx= numpy.isfinite(fQ4interp) self._logfQ_interp= interpolate.InterpolatedUnivariateSpline(\ Qs4interp[iindx],fQ4interp[iindx], k=3,ext=3) return sphericaldf.sample(self,R=R,z=z,phi=phi,n=n, return_orbit=return_orbit,rmin=rmin) def fQ(self,Q): """ NAME: fQ PURPOSE Calculate the f(Q) portion of an Osipkov-Merritt Hernquist distribution function INPUT: Q - The Osipkov-Merritt 'energy' E-L^2/[2ra^2] (can be Quantity) OUTPUT: fQ - The value of the f(Q) portion of the DF HISTORY: 2021-02-07 - Written - Bovy (UofT) """ return self._edf.fE(-Q)